cargo-asm
Use cargo-show-asm (cargo asm) to dump the assembly that rustc/LLVM actually produced for a Rust function, then read it to answer codegen questions. Don't speculate — read the asm.
When to Reach for It
- "Is this loop auto-vectorized?"
- "Does the compiler emit
popcnt/vpopcntq/cnt.16b/udot/vpshufbhere?" - "Is this branch eliminated / this bounds check elided / this function inlined?"
- "How does codegen differ between aarch64 and x86-64-v3?"
- Verifying a
#[inline],#[cold], ortarget_featurechange had the intended effect.
Preflight
which cargo-asm && cargo asm --version
If absent: cargo install cargo-show-asm. Don't install without asking the user.
Basic Usage
cargo asm matches against function paths, not raw symbols. Match is by substring of the demangled path.
# Library function, by partial path:
cargo asm -p <crate> --lib <substring>
# Specific binary / bench / example / integration test:
cargo asm -p <crate> --bin <name> <fn>
cargo asm -p <crate> --bench <name> <fn>
cargo asm -p <crate> --example <name> <fn>
cargo asm -p <crate> --test <name> <fn>
# List candidates when the substring is ambiguous (or returns "Can't find..."):
cargo asm -p <crate> --lib # prints the index of items
Useful flags:
--rust— interleave Rust source lines (extremely helpful for finding the right block).--simplify— drop cfi/debug noise.-C target-cpu=<cpu>— re-tune codegen (e.g.x86-64-v3,native,apple-m1).--target <triple>— cross-target inspection (needs the target installed viarustup target add).--full-name— show full demangled paths in the index.--context N— also dump callees up to N deep.
The Hidden Problem: Inlined / Internal Functions
cargo asm can only show functions that are actually emitted into the artifact. Two common reasons a function "disappears":
#[inline]or#[inline(always)]: fully inlined into callers; no standalone body emitted.- No public caller in the artifact: a generic or internal
pubfn that nothing in the crate's lib/bin/bench/test invokes won't appear.
If cargo asm -p crate --lib MyFn returns "Can't find any items matching ...", that's almost always the cause.
The Pinning Recipe
Add a temporary examples/<name>.rs that pins a non-inlined entry point calling the function you want to inspect. This is the most reliable way to look at an #[inline]-marked hot path with the exact monomorphization you care about.
//! examples/probe_asm.rs — temporary, delete after inspection.
use mycrate::Thing;
#[inline(never)]
#[unsafe(no_mangle)]
pub fn probe(t: &Thing, input: &[u64], out: &mut Vec<u32>) {
out.clear();
out.extend(t.hot_method(input));
}
fn main() {
// Use the probe so the linker keeps it.
let t = Thing::new();
let mut out = Vec::new();
probe(&t, &[0; 1024], &mut out);
std::hint::black_box(&out);
}
Then:
cargo asm -p mycrate --example probe_asm probe
#[inline(never)] keeps probe itself emitted; #[unsafe(no_mangle)] (Rust 2024) gives it a stable symbol name. The body inside hot_method is still inlined into probe, so you see its real codegen in context.
Delete the example file when done unless the user asks to keep it as a verification aid.
Cross-Target Comparison
For "does this vectorize on x86?" use rustc directly on a standalone reproducer — it's faster than configuring a full cross build of the workspace.
mkdir -p /tmp/asmcheck && cd /tmp/asmcheck
cat > probe.rs <<'EOF'
#![crate_type = "lib"]
#[unsafe(no_mangle)]
pub fn sum_popcnt(xs: &[u64]) -> u32 {
xs.iter().map(|x| x.count_ones()).sum()
}
EOF
for cpu in x86-64 x86-64-v2 x86-64-v3 x86-64-v4; do
rustc -O --emit=asm --target x86_64-unknown-linux-gnu -C target-cpu=$cpu probe.rs
cp probe.s probe_$cpu.s
done
# Inspect: awk '/^sum_popcnt:/,/\.cfi_endproc/' probe_x86-64-v3.s
Requires rustup target add x86_64-unknown-linux-gnu (or the relevant triple).
Vectorization Tell-Tales (general)
Across architectures, look for:
- Wide register prefixes in the hot loop:
xmm/ymm/zmmon x86,q<n>/v<n>.*on aarch64. - Multiple parallel accumulators (e.g.
v0,v1,v2,v3summed after the loop) — sign LLVM unrolled for ILP. - Large stride per iteration:
add x, x, #128/subs ..., #16(16 × u64) on aarch64;add $64, %rdion x86. - A scalar tail block after the SIMD loop (handles
len % vector_width).
If the hot loop only uses general-purpose registers (rax, x0–x30) with a ldr/mov + scalar op + b.ne, it's not vectorized.
Common Pitfalls
cargo checkthencargo asmis fine, butcargo asmtriggers its own release build withdebuginfo=2. Build times can surprise you on first run; subsequent runs are incremental.- Generics:
cargo asmneeds a monomorphized instance. Either call the generic with a concrete type from a pinned probe, or pass the full monomorphized path:cargo asm -p c --lib 'mycrate::foo::<u64>'. - Profiles: defaults to
release. Use--devonly if you're debugging codegen of the dev profile specifically — most autovec questions are release-only. opt-level: if the crate setsopt-level = 1or LTO settings in[profile.release], the asm will reflect that. Mention this when reporting findings.target-cpu = nativein.cargo/config.toml: results are host-specific. State the effective target-cpu in the report.
Reporting Findings
When verifying autovec for the user, your report should include:
- Exact
cargo asmcommand used. - Target + target-cpu (effective, not just default).
- The hot-loop block (label-to-label), not the full dump.
- A one-line identification of the SIMD idiom (e.g. "ARMv8.2 NEON popcount via
cnt+udot"). - Stride per iteration and number of parallel accumulators.
- If it's not vectorized, the reason (scalar dependency chain, unknown trip count and
opt-level,panicin the loop body inhibiting hoisting, etc.) — read the asm to confirm, don't guess.