Summary
--lib archives are compiled without -ffunction-sections -fdata-sections. On
ELF targets that makes --gc-sections at the consumer's link only able to
discard whole objects, so a library-mode consumer cannot recover the size win
that 0.0.36 gives the executable lane. Adding the two flags to the library
archive's cflags takes our Android .so from 852,720 to 674,616 bytes
(-21%) with no other change.
Why this isn't a bug report about the executable lane
0.0.36's section elimination is deliberately scoped, and the note beside it is
clear — backend/native-toolchain.js:
Archives and compile-only object/library recipes deliberately do not use the
link half. In particular, --lib preserves its established object and
archive contract; section GC is an executable-link optimization only.
We agree with that scoping: the link half belongs to whoever links. Our ask is
only about the compile half, which is not an optimization the consumer can
apply retroactively — by the time we get entry.lib.a, the section granularity
is already fixed.
The asymmetry this creates is platform-shaped, and matches the matrix in
executableSectionEliminationFlags:
| Target |
Consumer can strip a --lib archive? |
| Apple / Mach-O |
yes — ld64 dead-strips per symbol subsection, no compile flag needed |
| ELF, COFF |
no — needs -ffunction-sections at compile time, which only scriptc can pass |
So on iOS we fixed this ourselves with -Wl,-dead_strip (409,232 -> 232,208
bytes, -43%). On Android we can only get partway.
Repro
scriptc 0.0.36, NDK r27 (aarch64-linux-android26-clang++), arm64-v8a,
consumer link is a -shared JNI library, all sizes after llvm-strip.
scriptc --lib --profile … -o entry.lib.a
aarch64-linux-android26-clang++ shell.cc -shared -fPIC -O2 \
-static-libstdc++ entry.lib.a -llog -o libapp.so <flags>
llvm-strip libapp.so
| Archive built by |
Consumer flags |
libapp.so |
| stock 0.0.36 |
none |
1,406,488 |
| stock 0.0.36 |
--gc-sections |
1,354,832 |
| stock 0.0.36 |
--gc-sections --exclude-libs,ALL |
852,720 |
+ -ffunction-sections -fdata-sections |
--gc-sections --exclude-libs,ALL |
674,616 |
(--exclude-libs,ALL is needed on our side regardless: a shared library
exports every default-visibility symbol, which roots the whole archive and
leaves GC nothing to collect. That part is our bug, and it is fixed.)
The last row was produced by adding the two flags to the cflags array in
compileLibArchive and nothing else. It yields 1,544 .text.* sections in the
archive, and the resulting .so loads under dlopen(RTLD_NOW) on an
arm64-v8a emulator with all JNI entry points resolvable.
Suggested change
In compileLibArchive, alongside the existing -DSCR_LIB:
"-ffunction-sections", "-fdata-sections",
This is compile-side only and does not touch the archive's link contract.
A consumer who does not pass --gc-sections gets an output that is equivalent
but not bit-for-bit reproducible against the old one — worth stating precisely,
since "no change" would be too strong:
|
stock archive |
+ function-sections |
libapp.so size |
1,406,488 |
1,406,488 |
| sections |
26 |
26 |
.text size |
0xb5af8 |
0xb5af8 |
| dynamic exports |
2,751 |
2,751 (identical symbol names) |
| undefined symbols |
0 |
0 |
DT_NEEDED |
4 |
4 |
Same size, same section count, same .text size, same exported symbol set.
The files do differ — 158,124 bytes — but the difference is the order of
functions within .text, which is what per-function sections change. Nothing
is added or discarded unless the consumer asks for --gc-sections.
If you would rather not perturb existing output at all, an opt-in flag works
equally well for us.
Environment
scriptc 0.0.36 · NDK r27.0.12077973 · arm64-v8a, minSdk 26 · macOS host ·
consumer is janela, which builds mobile
apps by linking a --lib archive into a UIKit / JNI shell.
Summary
--libarchives are compiled without-ffunction-sections -fdata-sections. OnELF targets that makes
--gc-sectionsat the consumer's link only able todiscard whole objects, so a library-mode consumer cannot recover the size win
that 0.0.36 gives the executable lane. Adding the two flags to the library
archive's
cflagstakes our Android.sofrom 852,720 to 674,616 bytes(-21%) with no other change.
Why this isn't a bug report about the executable lane
0.0.36's section elimination is deliberately scoped, and the note beside it is
clear —
backend/native-toolchain.js:We agree with that scoping: the link half belongs to whoever links. Our ask is
only about the compile half, which is not an optimization the consumer can
apply retroactively — by the time we get
entry.lib.a, the section granularityis already fixed.
The asymmetry this creates is platform-shaped, and matches the matrix in
executableSectionEliminationFlags:--libarchive?-ffunction-sectionsat compile time, which only scriptc can passSo on iOS we fixed this ourselves with
-Wl,-dead_strip(409,232 -> 232,208bytes, -43%). On Android we can only get partway.
Repro
scriptc 0.0.36, NDK r27 (
aarch64-linux-android26-clang++), arm64-v8a,consumer link is a
-sharedJNI library, all sizes afterllvm-strip.libapp.so--gc-sections--gc-sections --exclude-libs,ALL-ffunction-sections -fdata-sections--gc-sections --exclude-libs,ALL(
--exclude-libs,ALLis needed on our side regardless: a shared libraryexports every default-visibility symbol, which roots the whole archive and
leaves GC nothing to collect. That part is our bug, and it is fixed.)
The last row was produced by adding the two flags to the
cflagsarray incompileLibArchiveand nothing else. It yields 1,544.text.*sections in thearchive, and the resulting
.soloads underdlopen(RTLD_NOW)on anarm64-v8a emulator with all JNI entry points resolvable.
Suggested change
In
compileLibArchive, alongside the existing-DSCR_LIB:This is compile-side only and does not touch the archive's link contract.
A consumer who does not pass
--gc-sectionsgets an output that is equivalentbut not bit-for-bit reproducible against the old one — worth stating precisely,
since "no change" would be too strong:
libapp.sosize.textsizeDT_NEEDEDSame size, same section count, same
.textsize, same exported symbol set.The files do differ — 158,124 bytes — but the difference is the order of
functions within
.text, which is what per-function sections change. Nothingis added or discarded unless the consumer asks for
--gc-sections.If you would rather not perturb existing output at all, an opt-in flag works
equally well for us.
Environment
scriptc 0.0.36 · NDK r27.0.12077973 · arm64-v8a, minSdk 26 · macOS host ·
consumer is janela, which builds mobile
apps by linking a
--libarchive into a UIKit / JNI shell.