From 17936ecbfcf913736027f72da771d595b0cb7caa Mon Sep 17 00:00:00 2001 From: Zul-Qarnain Date: Fri, 25 Sep 2026 01:03:36 +0600 Subject: [PATCH 1/5] ci: integrate callgrind benchmarks using iai-callgrind --- .github/workflows/checks.yml | 20 +++ Cargo.lock | 104 +++++++++++++ Cargo.toml | 6 + benches/callgrind.rs | 276 +++++++++++++++++++++++++++++++++++ 4 files changed, 406 insertions(+) create mode 100644 benches/callgrind.rs diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 441db727..9e84c42a 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -17,6 +17,26 @@ jobs: run: cargo bench --no-run - name: all features run: cargo bench --all-features --no-run + callgrind: + name: Callgrind + runs-on: ubuntu-latest + steps: + - name: checkout + uses: actions/checkout@v7 + - name: install + uses: dtolnay/rust-toolchain@nightly + - name: install valgrind + run: | + sudo apt-get update -y + sudo apt-get install -y valgrind + - name: install iai-callgrind-runner + run: | + version=$(cargo metadata --format-version=1 | jq -r '.packages[] | select(.name == "iai-callgrind").version') + cargo install iai-callgrind-runner --version "$version" + - name: default features + run: cargo bench --bench callgrind + - name: all features + run: cargo bench --bench callgrind --all-features practices: name: Practices runs-on: ubuntu-latest diff --git a/Cargo.lock b/Cargo.lock index 6ab62b8e..64b61a57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -53,6 +53,15 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -293,6 +302,27 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", +] + [[package]] name = "either" version = "1.18.0" @@ -394,6 +424,42 @@ version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +[[package]] +name = "iai-callgrind" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c9e864ad001cbbf35a64f7b574998823b8622bb483a25246fa1b0e2b3888686" +dependencies = [ + "bincode", + "derive_more", + "iai-callgrind-macros", + "iai-callgrind-runner", +] + +[[package]] +name = "iai-callgrind-macros" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9cee6c3bc32b63ff40e5e56190b1276d2bd0e0b319aa6f998864c27865d3b4e" +dependencies = [ + "derive_more", + "proc-macro-error2", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn 2.0.119", +] + +[[package]] +name = "iai-callgrind-runner" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a82491521e6505b06a824274712a1307904e7cdfdf047735f7dfdb28b5f7a337" +dependencies = [ + "serde", +] + [[package]] name = "indexmap" version = "2.14.1" @@ -542,6 +608,28 @@ dependencies = [ "toml_edit", ] +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "proc-macro2" version = "1.0.107" @@ -615,6 +703,15 @@ version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustversion" version = "1.0.23" @@ -630,6 +727,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + [[package]] name = "serde" version = "1.0.229" @@ -705,6 +808,7 @@ dependencies = [ "criterion", "defmt", "encase", + "iai-callgrind", "malloc_size_of", "rayon", "serde_core", diff --git a/Cargo.toml b/Cargo.toml index ec729e4f..1323c453 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ malloc_size_of = { version = "0.1", optional = true, default-features = false } [dev-dependencies] serde_test = "1.0" criterion = "0.8" +iai-callgrind = "0.14" [[test]] name = "arbitrary" @@ -75,6 +76,11 @@ name = "bench" path = "benches/bench.rs" harness = false +[[bench]] +name = "callgrind" +path = "benches/callgrind.rs" +harness = false + [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] diff --git a/benches/callgrind.rs b/benches/callgrind.rs new file mode 100644 index 00000000..d2076eb6 --- /dev/null +++ b/benches/callgrind.rs @@ -0,0 +1,276 @@ +use { + iai_callgrind::{ + library_benchmark, + library_benchmark_group, + main, + }, + smallvec::SmallVec, + std::hint::black_box, +}; + +const INLINE_CAP: usize = 16; +const SPILLED_COUNT: usize = 100; + +// ========================================================================= +// PUSH +// ========================================================================= + +#[library_benchmark] +fn bench_smallvec_push_inline() -> SmallVec { + let mut v = SmallVec::::new(); + for i in 0..(INLINE_CAP as u64) { + v.push(black_box(i)); + } + v +} + +#[library_benchmark] +fn bench_smallvec_push_spilled() -> SmallVec { + let mut v = SmallVec::::new(); + for i in 0..(SPILLED_COUNT as u64) { + v.push(black_box(i)); + } + v +} + +#[library_benchmark] +fn bench_vec_push() -> Vec { + let mut v = Vec::with_capacity(INLINE_CAP); + for i in 0..(INLINE_CAP as u64) { + v.push(black_box(i)); + } + v +} + +// ========================================================================= +// POP +// ========================================================================= + +#[library_benchmark] +fn bench_smallvec_pop_inline() -> u64 { + let mut v = SmallVec::::new(); + for i in 0..(INLINE_CAP as u64) { + v.push(black_box(i)); + } + let mut sum = 0; + while let Some(val) = v.pop() { + sum += black_box(val); + } + sum +} + +#[library_benchmark] +fn bench_smallvec_pop_spilled() -> u64 { + let mut v = SmallVec::::new(); + for i in 0..(SPILLED_COUNT as u64) { + v.push(black_box(i)); + } + let mut sum = 0; + while let Some(val) = v.pop() { + sum += black_box(val); + } + sum +} + +// ========================================================================= +// INSERT & REMOVE +// ========================================================================= + +#[library_benchmark] +fn bench_smallvec_insert_remove_inline() -> SmallVec { + let mut v = SmallVec::::new(); + for i in 0..((INLINE_CAP - 1) as u64) { + v.push(black_box(i)); + } + v.insert(black_box(4), black_box(999)); + let _ = v.remove(black_box(4)); + v +} + +#[library_benchmark] +fn bench_smallvec_insert_remove_spilled() -> SmallVec { + let mut v = SmallVec::::new(); + for i in 0..(SPILLED_COUNT as u64) { + v.push(black_box(i)); + } + v.insert(black_box(50), black_box(999)); + let _ = v.remove(black_box(50)); + v +} + +// ========================================================================= +// EXTEND & FROM_SLICE +// ========================================================================= + +#[library_benchmark] +fn bench_smallvec_from_slice_inline() -> SmallVec { + let data = [42u64; INLINE_CAP]; + SmallVec::::from(black_box(data.as_slice())) +} + +#[library_benchmark] +fn bench_smallvec_from_slice_spilled() -> SmallVec { + let data = [42u64; SPILLED_COUNT]; + SmallVec::::from(black_box(data.as_slice())) +} + +#[library_benchmark] +fn bench_smallvec_extend_from_slice_inline() -> SmallVec { + let mut v = SmallVec::::new(); + let data = [42u64; INLINE_CAP]; + v.extend_from_slice(black_box(&data)); + v +} + +#[library_benchmark] +fn bench_smallvec_extend_from_slice_spilled() -> SmallVec { + let mut v = SmallVec::::new(); + let data = [42u64; SPILLED_COUNT]; + v.extend_from_slice(black_box(&data)); + v +} + +// ========================================================================= +// ARRAY / BUF CONVERSION +// ========================================================================= + +#[library_benchmark] +fn bench_smallvec_from_array_inline() -> SmallVec { + SmallVec::::from(black_box([1u64; INLINE_CAP])) +} + +#[library_benchmark] +fn bench_smallvec_from_elem_spilled() -> SmallVec { + smallvec::from_elem(black_box(1u64), black_box(SPILLED_COUNT)) +} + +// ========================================================================= +// ITERATION & DRAIN +// ========================================================================= + +#[library_benchmark] +fn bench_smallvec_into_iter_inline() -> u64 { + let mut v = SmallVec::::new(); + for i in 0..(INLINE_CAP as u64) { + v.push(black_box(i)); + } + v.into_iter().fold(0, |acc, x| acc + black_box(x)) +} + +#[library_benchmark] +fn bench_smallvec_into_iter_spilled() -> u64 { + let mut v = SmallVec::::new(); + for i in 0..(SPILLED_COUNT as u64) { + v.push(black_box(i)); + } + v.into_iter().fold(0, |acc, x| acc + black_box(x)) +} + +#[library_benchmark] +fn bench_smallvec_drain_inline() -> u64 { + let mut v = SmallVec::::new(); + for i in 0..(INLINE_CAP as u64) { + v.push(black_box(i)); + } + let sum = v.drain(..).fold(0, |acc, x| acc + black_box(x)); + black_box(v); + sum +} + +#[library_benchmark] +fn bench_smallvec_drain_spilled() -> u64 { + let mut v = SmallVec::::new(); + for i in 0..(SPILLED_COUNT as u64) { + v.push(black_box(i)); + } + let sum = v.drain(..).fold(0, |acc, x| acc + black_box(x)); + black_box(v); + sum +} + +// ========================================================================= +// RETAIN MUT +// ========================================================================= + +#[library_benchmark] +fn bench_smallvec_retain_mut_inline() -> SmallVec { + let mut v = SmallVec::::new(); + for i in 0..(INLINE_CAP as u64) { + v.push(black_box(i)); + } + v.retain_mut(|x| *x % 2 == 0); + v +} + +#[library_benchmark] +fn bench_smallvec_retain_mut_spilled() -> SmallVec { + let mut v = SmallVec::::new(); + for i in 0..(SPILLED_COUNT as u64) { + v.push(black_box(i)); + } + v.retain_mut(|x| *x % 2 == 0); + v +} + +// ========================================================================= +// GROUPS & MAIN +// ========================================================================= + +library_benchmark_group!( + name = push_group; + benchmarks = + bench_smallvec_push_inline, + bench_smallvec_push_spilled, + bench_vec_push, +); + +library_benchmark_group!( + name = pop_group; + benchmarks = + bench_smallvec_pop_inline, + bench_smallvec_pop_spilled, +); + +library_benchmark_group!( + name = insert_remove_group; + benchmarks = + bench_smallvec_insert_remove_inline, + bench_smallvec_insert_remove_spilled, +); + +library_benchmark_group!( + name = slice_group; + benchmarks = + bench_smallvec_from_slice_inline, + bench_smallvec_from_slice_spilled, + bench_smallvec_extend_from_slice_inline, + bench_smallvec_extend_from_slice_spilled, +); + +library_benchmark_group!( + name = array_group; + benchmarks = + bench_smallvec_from_array_inline, + bench_smallvec_from_elem_spilled, +); + +library_benchmark_group!( + name = iteration_group; + benchmarks = + bench_smallvec_into_iter_inline, + bench_smallvec_into_iter_spilled, + bench_smallvec_drain_inline, + bench_smallvec_drain_spilled, + bench_smallvec_retain_mut_inline, + bench_smallvec_retain_mut_spilled, +); + +main!( + library_benchmark_groups = + push_group, + pop_group, + insert_remove_group, + slice_group, + array_group, + iteration_group, +); From 622a73c33b7212147829c41f1c10dd58dbe6913d Mon Sep 17 00:00:00 2001 From: Zul-Qarnain Date: Fri, 25 Sep 2026 01:09:26 +0600 Subject: [PATCH 2/5] style: fix formatting in benches/callgrind.rs --- benches/callgrind.rs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/benches/callgrind.rs b/benches/callgrind.rs index d2076eb6..9f3c605a 100644 --- a/benches/callgrind.rs +++ b/benches/callgrind.rs @@ -2,10 +2,10 @@ use { iai_callgrind::{ library_benchmark, library_benchmark_group, - main, + main }, smallvec::SmallVec, - std::hint::black_box, + std::hint::black_box }; const INLINE_CAP: usize = 16; @@ -266,11 +266,10 @@ library_benchmark_group!( ); main!( - library_benchmark_groups = - push_group, - pop_group, - insert_remove_group, - slice_group, - array_group, - iteration_group, + library_benchmark_groups = push_group, + pop_group, + insert_remove_group, + slice_group, + array_group, + iteration_group ); From 6c5d37eed12846e4fdf4b46bfae80a0e265f865b Mon Sep 17 00:00:00 2001 From: Zul-Qarnain Date: Fri, 25 Sep 2026 10:45:43 +0600 Subject: [PATCH 3/5] ci: combine valgrind and runner setup into default features step --- .github/workflows/checks.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 9e84c42a..1679e4c1 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -25,16 +25,13 @@ jobs: uses: actions/checkout@v7 - name: install uses: dtolnay/rust-toolchain@nightly - - name: install valgrind + - name: default features run: | sudo apt-get update -y sudo apt-get install -y valgrind - - name: install iai-callgrind-runner - run: | version=$(cargo metadata --format-version=1 | jq -r '.packages[] | select(.name == "iai-callgrind").version') cargo install iai-callgrind-runner --version "$version" - - name: default features - run: cargo bench --bench callgrind + cargo bench --bench callgrind - name: all features run: cargo bench --bench callgrind --all-features practices: From 5cd5e5ba0fc8f1ffbff202197a3aea8a553faf27 Mon Sep 17 00:00:00 2001 From: Zul-Qarnain Date: Fri, 25 Sep 2026 18:23:24 +0600 Subject: [PATCH 4/5] fix(callgrind): build benchmarks with debug symbols so callgrind can attribute events --- .github/workflows/checks.yml | 4 ++-- Cargo.toml | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 1679e4c1..c2f52a11 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -31,9 +31,9 @@ jobs: sudo apt-get install -y valgrind version=$(cargo metadata --format-version=1 | jq -r '.packages[] | select(.name == "iai-callgrind").version') cargo install iai-callgrind-runner --version "$version" - cargo bench --bench callgrind + cargo bench --bench callgrind --profile callgrind - name: all features - run: cargo bench --bench callgrind --all-features + run: cargo bench --bench callgrind --profile callgrind --all-features practices: name: Practices runs-on: ubuntu-latest diff --git a/Cargo.toml b/Cargo.toml index 1323c453..b859cd6b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,16 @@ opt-level = 3 debug = false strip = true +# Dedicated profile for the iai-callgrind benchmarks. Callgrind needs function +# symbols to attribute event counts, so we keep debug info on and stripping off, +# and disable LTO which would inline the benchmark functions. Without this the +# runner collects zero events for every benchmark. +[profile.callgrind] +inherits = "bench" +debug = 1 +strip = false +lto = false + [features] allocator-api = [] allocator-api2 = ["dep:allocator-api2", "allocator-api"] From 701a661a5e59dc22a1178d92f762f69e41ccab76 Mon Sep 17 00:00:00 2001 From: Zul-Qarnain Date: Fri, 25 Sep 2026 20:23:19 +0600 Subject: [PATCH 5/5] ci: keep LTO in callgrind profile; only stripping affects collection --- Cargo.toml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b859cd6b..6249c04a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,15 +18,15 @@ opt-level = 3 debug = false strip = true -# Dedicated profile for the iai-callgrind benchmarks. Callgrind needs function -# symbols to attribute event counts, so we keep debug info on and stripping off, -# and disable LTO which would inline the benchmark functions. Without this the -# runner collects zero events for every benchmark. +# Dedicated profile for the iai-callgrind benchmarks. Callgrind needs the +# function symbols in the binary to attribute event counts, so we disable +# stripping (and keep debug info for line-level detail). Without symbols the +# runner collects zero events for every benchmark. LTO is left on (inherited) +# since it does not affect collection and keeps the run representative. [profile.callgrind] inherits = "bench" debug = 1 strip = false -lto = false [features] allocator-api = []