Skip to content

Fold identical functions at link time with --icf=safe - #6547

Merged
Fedr merged 2 commits into
masterfrom
linker-icf-safe
Aug 7, 2026
Merged

Fold identical functions at link time with --icf=safe#6547
Fedr merged 2 commits into
masterfrom
linker-icf-safe

Conversation

@Fedr

@Fedr Fedr commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Identical Code Folding merges byte-identical function bodies at link time. #6535 turned it on for the Python bindings (--icf=all); this enables it for the MeshLib libraries themselves (libMRMesh.so & co) in non-Debug Linux builds, in the conservative safe mode.

safe folds only functions whose address is provably insignificant, so &SomeClass::method / callback pointers keep comparing unequal — the guarantee a public C++ library owes its users. all stays reserved for the bindings, where nothing observes wrapper addresses.

Which linker actually implements it

ICF is a linker feature, not a compiler one — Clang only forwards the option:

linker option in our CI
LLVM lld --icf={none,safe,all} already the linker for Linux+Clang builds (forced since the patchelf work-around above) — this is where the win comes from
GNU gold --icf=[none,all,safe]"folds ctors, dtors and functions whose pointers are definitely not taken" not used
mold --icf=[none,all,safe] not used
GNU ld (bfd) none default linker of the GCC rows — errors out on the option
Apple ld none — only the opt-out -no_deduplicate ("don't run deduplication pass in linker"), i.e. it already folds by default excluded by NOT APPLE
wasm-ld none — folding happens later, in Binaryen's duplicate-function-elimination excluded by NOT MR_EMSCRIPTEN
MSVC link /OPT:ICF already the Release default, nothing to do

So the answer for the GNU toolchain: ld.bfd has no ICF at all; gold and mold accept the very same spelling. GCC's -fipa-icf (on at -O2) is a compile-time analogue that folds within a translation unit only — it is not a substitute for cross-object folding of template instantiations.

There is a second, subtler reason the win is Clang-only: safe mode decides what may be folded from the address-significance table (.llvm_addrsig), which Clang emits by default while GCC emits nothing of the kind. So even gcc + mold --icf=safe would fold almost nothing; it takes --icf=all to fold GCC output, and that is exactly the mode we do not want here.

That is why the flag goes inside the existing MESHLIB_HAVE_LLD block instead of getting a probe of its own: there lld is already established as the linker by its own check_linker_flag, and no row outside it can use the option anyway. Debug is excluded through the same $<$<NOT:$<CONFIG:Debug>>:…> genex that the neighbouring -Wl,-x uses.

A guarding check_linker_flag(CXX "-Wl,--icf=safe" …) was the first attempt and is a trap worth recording: the module replaces CMAKE_REQUIRED_LINK_OPTIONS with the flag under test (set(CMAKE_REQUIRED_LINK_OPTIONS "${_flag}") in Internal/CheckLinkerFlag.cmake), so the -fuse-ld=lld passed alongside it is dropped and the probe tests the image's default linker — ld.bfd, which has no ICF. The check therefore reported Failed on the Clang 21 rows as well, and the flag was silently never applied while CI stayed green.

Practically this means the Clang-built artifacts — the linux-vcpkg package, the AppImage and the manylinux wheels, all built with clang++ in the rockylinux8-vcpkg image — get smaller, while the GCC-built .debs are bit-for-bit unaffected until they too move to lld/mold (possible follow-up).

Measured effect

A/B on the same commit, same image, Release / Clang 21 / lld, the flag being the only difference (throwaway branch, both runs measured identically). Raw sizes in build/Release/bin:

binary x64 before x64 after x64 Δ arm64 Δ
libMeshLibC2.so 24,636,784 24,284,528 −1.4% −1.5%
libMRMesh.so 9,827,240 9,610,152 −2.2% −2.8%
libMRViewer.so 9,237,736 8,946,920 −3.1% −3.4%
libMRVoxels.so 8,357,640 7,976,712 −4.6% −5.3%
libMRCommonPlugins.so 2,040,888 1,946,680 −4.6% −2.9%
MRTest 3,761,368 3,650,776 −2.9% −3.4%
all .so 63,665,400 62,285,048 −2.2% (−1.35 MB) −2.4% (−1.57 MB)

The compressed package moves much less: meshlib_linux-vcpkg-x64.tar.xz 68,720,612 → 68,665,336 B (−0.08%), arm64 65,517,604 → 65,005,316 B (−0.78%). That is expected — what ICF removes is duplicated code, which xz was already encoding as back-references, so the win is in installed footprint and resident pages rather than in download size. (Unlike #6535, where the bindings' zip win tracked the raw win; those duplicates were bulkier and more numerous.)

Two caveats so the per-file numbers aren't over-read:

  • Every x64 delta is a multiple of 4 KB and every arm64 delta a multiple of 64 KB: -z separate-loadable-segments (the patchelf work-around directly above) pads each loadable segment to max-page-size, so file sizes quantize. Several small arm64 libraries show a flat 0 while having shrunk by less than one page. The aggregate row is the number to trust.
  • The two package baselines come from the master run of the same commit, which stamps a real version instead of 0.0.0 — a few bytes of difference, but it makes the sub-percent package figures less airtight than the raw ones.

Link time was not measured; I have timings only for the ICF side, so there is no honest comparison to quote yet.

Testing

Unit, C-unit, Python sanity and regression tests run in the Linux legs with folding active. Windows and macOS are disabled for this PR: the code path is guarded by UNIX AND NOT APPLE, so neither can be reached.

Enable lld/gold/mold ICF for non-Debug Linux builds of the MeshLib
libraries. Guarded by check_linker_flag, so GNU ld (no ICF) is a no-op.
check_linker_flag replaces CMAKE_REQUIRED_LINK_OPTIONS with the flag under
test, so the -fuse-ld=lld it needed was dropped and the check probed the
image's default ld.bfd -- it failed on every row.
@Fedr
Fedr merged commit 186410a into master Aug 7, 2026
45 checks passed
@Fedr
Fedr deleted the linker-icf-safe branch August 7, 2026 10:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants