Repository navigation
Description
perry compile --report-size builds its attribution by summing symbol sizes. It never groups by address, so every symbol that identical-code folding collapsed onto a shared address is charged again, once per name.
Measured on main c8cf450 (v0.5.1592), Linux x86-64 / ELF, comparing the sum of FUNC symbol sizes against the sum over distinct addresses (addr != 0):
| binary | FUNC syms | distinct addrs | report attributes | actually in .text |
over-count |
|---|---|---|---|---|---|
console.log hello, --no-auto-optimize |
13,124 | 12,298 | 10,751,930 B | 10,515,200 B | 236,730 B (2.2 %) |
12-line rich.ts, auto-optimized |
9,527 | 8,767 | 7,469,826 B | 7,249,496 B | 220,330 B (2.9 %) |
The user-visible consequence: the top suggestion is already-recovered bytes
On the hello-world binary the report's Top suggestion is:
duplicate-function-body (~41.6 KiB): 193 symbols share one identical body (222 B each, 41.6 KiB wasted):
<perry_runtime::tls_hot::HotKey<…>>::resolve_and_cache
The observation is correct — HotKey<T> uses T only in PhantomData<*const T> (its fields are slot: &'static SlotId, resolve: fn() -> Result<*mut u8, AccessError>, arm_guard: fn(u32)), so resolve_and_cache really is type-independent and its 193 instantiations really are byte-identical.
But the conclusion is wrong, because the linker already folded them:
$ readelf -sW hello | awk '$4=="FUNC"' | grep -c resolve_and_cache
193
$ readelf -sW hello | awk '$4=="FUNC"' | grep resolve_and_cache | awk '{print $2}' | sort -u | wc -l
1 # all 193 at 0x00000000000d34a0There is no 41.6 KiB to recover. I had the type-erasure fix written before checking the addresses.
Top folded groups in that binary, i.e. bytes the report invents:
| bytes reported | aliases | symbol |
|---|---|---|
| 42,624 | 193 | <perry_runtime::tls_hot::HotKey<_>>::resolve_and_cache |
| 16,940 | 15 | hashbrown::RawTable<(u64,u16)>::reserve_rehash::<…> |
| 13,530 | 11 | hashbrown::RawTable<(String,…)>::reserve_rehash::<…> |
| 12,870 | 12 | hashbrown::RawTable<(u64,…)>::reserve_rehash::<…> |
| 11,635 | 180 | a second <perry_runtime::tls_hot::HotKey<_>> body |
| 11,592 | 10 | hashbrown::RawTable<(u64,String)>::reserve_rehash::<…> |
Why it matters
--report-size is the tool anyone doing binary-size work uses to pick a target (it is what I used after #10382). Two of its outputs mislead:
duplicate-function-bodyflags ICF-folded aliases as waste. Every entry that shares an address is a false positive, and the two largest entries here are exactly that.By crate,largestandGeneric monomorphizationall inherit the 2–3 % over-count, and it is not spread evenly — it concentrates in whichever crate happens to monomorphize a type-independent function many times, so cross-crate ranking is skewed too.generic-monomorphizationhas the same issue for any family whose instantiations fold.
Suggested fix
- Group symbols by address before summing; charge each address once. (Ties: attribute to one name deterministically, or split evenly — either is better than N×.)
- Suppress a
duplicate-function-bodyfinding when the duplicates share an address, or relabel it "already folded by the linker (no action)". A duplicate-body finding is only actionable when the duplicates sit at different addresses — which is the genuinely useful case the check should keep reporting. - Consider printing the folded total as its own line ("
ICF folded N symbols onto M addresses, saving X"), which is real and worth knowing.
Notes
llvm-nmreturns zero symbols and exit code 0 on these binaries even though they are not stripped and carry a 13,530-entry.symtab;readelf -sWis correct. Worth knowing if the reproduction is scripted.- Verified on Linux/ELF only. macOS/Mach-O should be checked separately — the report already documents that Mach-O sizes are a next-symbol-distance upper bound, which is a second, independent source of over-count there.
Activity