Skip to content

--report-size over-counts by 2-3%: it sums symbol sizes without grouping by address, so ICF-folded aliases are charged once per name #10571

Description

@proggeramlug

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:

  1. 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.
  2. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions