diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..5fda1d8 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "third_party/c_library_v2"] + path = third_party/c_library_v2 + url = https://github.com/mavlink/c_library_v2.git diff --git a/Cargo.lock b/Cargo.lock index 79c4a7c..8389e9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -557,6 +557,7 @@ version = "0.1.0" dependencies = [ "anyhow", "bytes", + "cc", "criterion", "dev-utils", "dhat", diff --git a/Cargo.toml b/Cargo.toml index 603d1bb..cba98a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,12 @@ tokio-util = { version = "0.7", features = ["codec"] } [features] default = ["std"] std = [] +# Enables the C reference (c_library_v2) decode benchmark arm. Requires the +# third_party/c_library_v2 git submodule and a C compiler. +bench-c-reference = [] + +[build-dependencies] +cc = "1.2" [dev-dependencies] anyhow = "1.0" diff --git a/benches/bench.rs b/benches/bench.rs index dcd7fdf..c07f032 100644 --- a/benches/bench.rs +++ b/benches/bench.rs @@ -10,6 +10,58 @@ use rand::{prelude::StdRng, SeedableRng}; use tokio_stream::StreamExt; use tokio_util::codec::{Decoder, FramedRead}; +#[cfg(feature = "bench-c-reference")] +mod c_reference { + use std::{ffi::c_int, mem::MaybeUninit, ptr}; + + pub struct BenchState(*mut std::ffi::c_void); + + extern "C" { + fn mavlink_codec_bench_state_new() -> *mut std::ffi::c_void; + fn mavlink_codec_bench_state_reset(state: *mut std::ffi::c_void); + fn mavlink_codec_bench_state_free(state: *mut std::ffi::c_void); + fn mavlink_codec_bench_state_decode( + state: *mut std::ffi::c_void, + data: *const u8, + len: usize, + ) -> c_int; + fn mavlink_codec_bench_state_last_message( + state: *const std::ffi::c_void, + ) -> *const std::ffi::c_void; + } + + impl BenchState { + pub fn new() -> Self { + let state = unsafe { mavlink_codec_bench_state_new() }; + assert!(!state.is_null()); + Self(state) + } + + pub fn reset(&mut self) { + unsafe { mavlink_codec_bench_state_reset(self.0) }; + } + + pub fn decode(&mut self, data: &[u8]) -> c_int { + unsafe { mavlink_codec_bench_state_decode(self.0, data.as_ptr(), data.len()) } + } + + pub fn last_message_word(&self) -> u64 { + let msg = unsafe { mavlink_codec_bench_state_last_message(self.0) }; + let mut word = MaybeUninit::::uninit(); + unsafe { + ptr::copy_nonoverlapping(msg.cast(), word.as_mut_ptr(), 1); + word.assume_init() + } + } + } + + impl Drop for BenchState { + fn drop(&mut self) { + unsafe { mavlink_codec_bench_state_free(self.0) }; + } + } +} + fn add_random_v2_message(buf: &mut Vec, rng: &mut StdRng) { use rand::Rng; @@ -159,6 +211,29 @@ fn benchmark_decode(c: &mut Criterion) { ); }, ); + + #[cfg(feature = "bench-c-reference")] + group.bench_with_input( + BenchmarkId::new("c_library_v2", messages_count), + messages_count, + |b, &messages_count| { + let buf = buf.clone(); + + b.iter_batched( + || { + let mut state = c_reference::BenchState::new(); + state.reset(); + state + }, + |mut state| { + let count = state.decode(&buf); + black_box(count); + black_box(state.last_message_word()); + }, + criterion::BatchSize::SmallInput, + ); + }, + ); } group.finish(); diff --git a/benches/c_shim/shim.c b/benches/c_shim/shim.c new file mode 100644 index 0000000..942f835 --- /dev/null +++ b/benches/c_shim/shim.c @@ -0,0 +1,51 @@ +#include +#include +#include +#include + +#include + +struct mavlink_codec_bench_state { + mavlink_message_t rxmsg; + mavlink_status_t status; + mavlink_message_t r_message; + mavlink_status_t r_status; + mavlink_message_t last_message; +}; + +struct mavlink_codec_bench_state *mavlink_codec_bench_state_new(void) { + struct mavlink_codec_bench_state *state = + calloc(1, sizeof(struct mavlink_codec_bench_state)); + return state; +} + +void mavlink_codec_bench_state_reset(struct mavlink_codec_bench_state *state) { + memset(&state->rxmsg, 0, sizeof(state->rxmsg)); + memset(&state->status, 0, sizeof(state->status)); +} + +void mavlink_codec_bench_state_free(struct mavlink_codec_bench_state *state) { + free(state); +} + +/* Decodes `len` bytes one char at a time and returns the number of complete + * framed messages. Each completed frame is copied into `last_message` so the + * compiler cannot elide message materialization. */ +int mavlink_codec_bench_state_decode(struct mavlink_codec_bench_state *state, + const uint8_t *data, size_t len) { + int count = 0; + for (size_t i = 0; i < len; i++) { + if (mavlink_frame_char_buffer(&state->rxmsg, &state->status, data[i], + &state->r_message, + &state->r_status) == MAVLINK_FRAMING_OK) { + state->last_message = state->r_message; + count++; + } + } + return count; +} + +const mavlink_message_t * +mavlink_codec_bench_state_last_message(const struct mavlink_codec_bench_state *state) { + return &state->last_message; +} diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..f879185 --- /dev/null +++ b/build.rs @@ -0,0 +1,27 @@ +fn main() { + println!("cargo:rerun-if-changed=build.rs"); + + // The C reference shim is only compiled for the opt-in benchmark arm so that normal + // builds need neither a C compiler nor the c_library_v2 submodule. + if std::env::var_os("CARGO_FEATURE_BENCH_C_REFERENCE").is_none() { + return; + } + + let lib_root = std::path::Path::new("third_party/c_library_v2"); + assert!( + lib_root.join("ardupilotmega/mavlink.h").exists(), + "the `bench-c-reference` feature requires the c_library_v2 submodule; run \ + `git submodule update --init third_party/c_library_v2`" + ); + + println!("cargo:rerun-if-changed=benches/c_shim/shim.c"); + + cc::Build::new() + .file("benches/c_shim/shim.c") + .include(lib_root) + .warnings(false) + .flag_if_supported("-Wno-address-of-packed-member") + .flag_if_supported("-Wno-unused-parameter") + .flag_if_supported("-Wno-unused-function") + .compile("mavlink_c_shim"); +} diff --git a/third_party/c_library_v2 b/third_party/c_library_v2 new file mode 160000 index 0000000..e5697c4 --- /dev/null +++ b/third_party/c_library_v2 @@ -0,0 +1 @@ +Subproject commit e5697c4deaf09068560cb33da2a0283ad6229b3e