| name | analyze-rust-code-size |
| description | Analyze Rust binary code size and identify bloat from monomorphization, large functions, and macro expansion using cargo-llvm-lines and rustc flags. Use when binary is too large or compile times are slow. |
Analyze Rust Code Size & Monomorphization
Find code bloat, excessive monomorphization, and macro expansion issues that cause large binaries and slow compile times.
CRITICAL WORKFLOW RULE
ALWAYS capture full output to a file FIRST, THEN analyze it afterwards.
❌ WRONG (wasteful, loses data if grep fails):
cargo llvm-lines --bin mybin | grep "something" # BAD! Expensive command piped to grep
rustc -Z macro-stats ... 2>&1 | head -50 # BAD! You lose most of the data
✅ CORRECT (efficient, preserves all data):
# Step 1: Capture EVERYTHING to a file
cargo llvm-lines --bin mybin > /tmp/llvm-lines.txt 2>&1
# Step 2: Analyze the file however you want
grep "picante" /tmp/llvm-lines.txt
head -100 /tmp/llvm-lines.txt
cat /tmp/llvm-lines.txt | whatever-analysis-you-want
This applies to ALL expensive commands: cargo builds, rustc invocations, profiling tools, etc.
Prerequisites
# Install cargo-llvm-lines
cargo install cargo-llvm-lines
# Nightly rust for -Z flags
rustup install nightly
Tool 1: cargo-llvm-lines (Find Monomorphization Bloat)
Shows which functions generate the most LLVM IR lines, indicating monomorphization bloat.
Usage
# Step 1: Capture to file (takes a while, needs full rebuild)
cargo llvm-lines --bin <binary-name> -p <package-name> > /tmp/llvm-lines-<binary>.txt 2>&1
# Step 2: Analyze the file
head -100 /tmp/llvm-lines-<binary>.txt
grep "picante" /tmp/llvm-lines-<binary>.txt
grep "tokio" /tmp/llvm-lines-<binary>.txt
Interpreting Results
Output format:
Lines Copies Function name
----- ------ -------------
1952157 41947 (TOTAL)
234836 (12.0%) 50 (0.1%) picante::ingredient::derived::DerivedIngredient<DB,K,V>::access_scoped::{{closure}}
- Lines: Total LLVM IR lines generated by all copies of this function
- Copies: Number of monomorphized instances (different type parameters)
- % of total: Percentage of total code size
Red flags:
High lines, low copies: One function generating tons of code (e.g., 234,836 lines from 50 copies = 4,700 lines each!)
- Solution: Consider
dyn Trait, split function into smaller pieces, or reduce generic complexity
- Solution: Consider
High lines, high copies: Trait explosion (e.g., 100,000 lines from 5,000 copies)
- Solution: Use
dyn Trait, consolidate types, or reduce generic combinations
- Solution: Use
Closures with many lines: Often from large
impl Futureor complex generic contexts- Solution: Extract closure body into a separate non-generic function
Common Culprits
- Picante/Salsa derived queries (generates lots of code per query type)
- Tokio futures (each async fn with different concrete types)
- Serde serialize/deserialize for many types
- Iterator chains with many generic combinators
Tool 2: -Zmacro-stats (Find Macro Expansion Bloat)
Shows which macros expand to the most code.
Usage
# Step 1: Capture to file
RUSTFLAGS="-Zmacro-backtrace" \
cargo +nightly build --bin <binary> -p <package> > /tmp/macro-stats.txt 2>&1
# Step 2: Look for macro stats in the output
grep -i "macro" /tmp/macro-stats.txt
Note: -Zmacro-stats is an older flag that may not work on newer nightly. Use -Zunpretty=expanded to see full expansion:
# Step 1: Capture expanded code
cargo +nightly rustc --bin <binary> -p <package> -- -Zunpretty=expanded > /tmp/expanded.rs 2>&1
# Step 2: Analyze
wc -l /tmp/expanded.rs # Total lines after macro expansion
grep "picante" /tmp/expanded.rs | wc -l
Tool 3: -Zprint-type-sizes (Find Large Types)
Large types cause expensive memcpy operations and increase monomorphization cost.
Usage
# Step 1: Capture to file
cargo +nightly rustc --bin <binary> -p <package> -- -Zprint-type-sizes > /tmp/type-sizes.txt 2>&1
# Step 2: Analyze
head -100 /tmp/type-sizes.txt
grep "picante" /tmp/type-sizes.txt
sort -k2 -n /tmp/type-sizes.txt | tail -50 # Largest types
Interpreting Results
print-type-size type: `picante::ingredient::derived::Cell<...>`: 128 bytes
Red flags:
- Types > 100 bytes (expensive to copy)
- Deeply nested generic types (increase monomorphization)
Solutions:
- Box large fields
- Use references instead of owned types
- Simplify nested generics
Tool 4: -Zprint-mono-items (See ALL Monomorphized Items)
Shows every single generic function instantiation. WARNING: Very verbose!
Usage
# Step 1: Capture to file (will be HUGE)
cargo +nightly rustc --bin <binary> -p <package> -- -Zprint-mono-items=eager > /tmp/mono-items.txt 2>&1
# Step 2: Analyze patterns
grep "picante" /tmp/mono-items.txt | wc -l
grep "picante::ingredient::derived" /tmp/mono-items.txt | sort | uniq | wc -l
This tells you exactly which type combinations are being monomorphized.
Workflow: Finding Compile Time Bloat
Start with llvm-lines (fastest, most actionable):
cargo llvm-lines --bin ddc -p dodeca > /tmp/llvm-lines.txt 2>&1 head -50 /tmp/llvm-lines.txtIdentify top bloaters (functions with high lines × copies)
Check if it's from a specific crate:
grep "picante" /tmp/llvm-lines.txt | head -20 grep "tokio" /tmp/llvm-lines.txt | head -20 grep "serde" /tmp/llvm-lines.txt | head -20For macro-heavy crates, check expansion:
cargo +nightly rustc --bin ddc -- -Zunpretty=expanded > /tmp/expanded.rs 2>&1 wc -l /tmp/expanded.rsCheck type sizes if lots of memcpy/clone:
cargo +nightly rustc --bin ddc -- -Zprint-type-sizes > /tmp/type-sizes.txt 2>&1 sort -k2 -n /tmp/type-sizes.txt | tail -50
Solutions to Common Issues
Issue: Picante queries generate tons of code
Each #[derive(Query)] or #[salsa::query] generates lots of monomorphized code for runtime state, caching, etc.
Solutions:
- Reduce number of query types (consolidate similar queries)
- Use fewer concrete types with each query
- Consider if some queries can use
dyn Traitinternally
Issue: Large async fn closures
Async functions become impl Future which monomorphize for each concrete type.
Solutions:
- Extract logic into separate non-async helper functions
- Use
Box<dyn Future>for cold paths - Reduce generic parameters on async functions
Issue: Iterator chains
Each iterator combinator (.map().filter().collect()) with different types creates new monomorphizations.
Solutions:
- Extract complex iterator chains into helper functions
- Use explicit loops for cold paths
- Consider
Vec<Box<dyn Trait>>for heterogeneous collections
Summary
Golden rule: Capture → Analyze → Don't Waste Work
Tools in order of usefulness:
cargo llvm-lines- Find monomorphization bloat-Zprint-type-sizes- Find large types-Zunpretty=expanded- See macro expansion-Zprint-mono-items- Full monomorphization dump (very verbose)
Always save output to files first!