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 0x00000000000d34a0
There 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-body flags 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, largest and Generic monomorphization all 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-monomorphization has 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-body finding 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-nm returns zero symbols and exit code 0 on these binaries even though they are not stripped and carry a 13,530-entry .symtab; readelf -sW is 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.
perry compile --report-sizebuilds 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
FUNCsymbol sizes against the sum over distinct addresses (addr != 0):.textconsole.loghello,--no-auto-optimizerich.ts, auto-optimizedThe user-visible consequence: the top suggestion is already-recovered bytes
On the hello-world binary the report's
Top suggestionis:The observation is correct —
HotKey<T>usesTonly inPhantomData<*const T>(its fields areslot: &'static SlotId,resolve: fn() -> Result<*mut u8, AccessError>,arm_guard: fn(u32)), soresolve_and_cachereally is type-independent and its 193 instantiations really are byte-identical.But the conclusion is wrong, because the linker already folded them:
There 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:
<perry_runtime::tls_hot::HotKey<_>>::resolve_and_cachehashbrown::RawTable<(u64,u16)>::reserve_rehash::<…>hashbrown::RawTable<(String,…)>::reserve_rehash::<…>hashbrown::RawTable<(u64,…)>::reserve_rehash::<…><perry_runtime::tls_hot::HotKey<_>>bodyhashbrown::RawTable<(u64,String)>::reserve_rehash::<…>Why it matters
--report-sizeis 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
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.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.