.. _l-next-steps-proto-binary-size: Reducing the ``lib_onnx_proto`` binary size =========================================== :Date: 2026-08 **complete** Objective +++++++++ The CMake target ``lib_onnx_proto`` produces ``liblib_onnx_proto.so`` (the shared library linked by consumers that only need to parse and serialize ONNX models). That dependency should not pull in unrelated features or pay for one copy of every convenience wrapper generated for every message class. The primary objective is to reduce both: * the installed size of ``onnx_light/onnx_py/liblib_onnx_proto.so``; * the mapped code and read-only data needed at runtime. Size reduction must preserve the ONNX wire format, parser and serializer behavior, external-data support required by ordinary models, and the ability to exchange proto objects across onnx-light shared libraries. Post-mortem +++++++++++ The work was delivered as a sequence of small pull requests. Each one made the next step easier to measure or safer to deploy: .. list-table:: :header-rows: 1 :widths: 15 27 34 24 * - Pull request - Change - Result - Role in the sequence * - `PR #4204 `_ - Enabled function and data sections plus linker garbage collection on GNU/Clang, Apple, and MSVC builds. - Allowed unused code to be discarded, but no isolated size measurement was recorded. - Established the linker foundation used by the later reductions. * - `PR #4333 `_ - Added a cross-platform size reporter to CI and stripped Release wheel artifacts explicitly. - Reduced the installed Linux library from 2,178,120 to 1,810,208 bytes, a saving of 367,912 bytes (16.9%). - Turned binary size into an observable release metric before changing the ABI surface. * - `PR #4344 `_ - Hid symbols by default on ELF and Mach-O and introduced ``ONNX_LIGHT_PROTO_API`` for the required public and cross-library ABI. - Reduced the stripped library from 1,810,208 to 1,625,024 bytes and defined dynamic symbols from 2,128 to 1,191. - Let ``--gc-sections`` discard implementation details that had previously been retained only because they were exported. * - `PR #4349 `_ - Replaced per-message out-of-line convenience wrappers with the ``ProtoMessageAdapter`` CRTP adapter and shared the ``CopyFrom`` wire pipeline through a type-erased implementation. - Reduced the stripped library from 1,625,024 to 1,001,208 bytes and defined dynamic symbols from 1,191 to 652. - Removed the structural source of duplication rather than relying on compiler flags to fold it. * - `PR #4355 `_ - Added a tested 1.2 MiB installed-size budget to Linux CI. - Produced no immediate size reduction. - Converted the achieved size into a regression guard. The comparable stripped baseline therefore decreased from 1,810,208 to 1,001,208 bytes: 809,000 bytes, or 44.7%. Including Release stripping, the installed artifact decreased by 1,176,912 bytes (54.0%). The defined dynamic symbol count fell by 69.4%, from 2,128 to 652. The largest individual gain came from removing duplicated generated wrappers, not from a more aggressive optimization level. What worked ^^^^^^^^^^^ * Measuring the packaged library, ELF sections, exported symbols, and shared dependencies prevented file-size changes from being mistaken for code-size changes. Stripping reduced the installed artifact but did not reduce mapped executable code. * Reducing symbol visibility before consolidating wrappers separated two overlapping effects: dead-code elimination at link time and elimination of duplicated source-level implementations. * Keeping the type-specific wire-format core unchanged limited the refactoring risk. Compatibility entry points moved to an inline CRTP adapter, while parsing, serialization, size computation, and printing remained explicit per-message operations. * The sequence ``measure -> constrain exports -> remove duplication -> enforce budget`` kept every pull request reviewable and left a measurement after each architectural change. What remains ^^^^^^^^^^^^ The 1.00 MiB stretch target was reached without ``MinSizeRel``, LTO, or identical-code-folding experiments beyond the existing linker configuration. Those options were not pursued because their expected gain was smaller and more toolchain-dependent. Splitting verification, crypto, hashing, external data tools, and text printing into optional libraries may still save 100--300 KiB for parser-only consumers, but it changes target composition and dependency ownership and should be treated as a separate project. Measured baseline +++++++++++++++++ The current Linux x86-64 Release library was measured with OpenSSL enabled: .. list-table:: :header-rows: 1 :widths: 42 24 34 * - Metric - Size - Observation * - File on disk - 2,178,120 bytes - The ELF file is not stripped * - File after ``strip --strip-unneeded`` - 1,810,208 bytes - Immediate 16.9% packaging reduction * - Allocated ELF sections - 1,803,125 bytes - Approximate mapped image * - ``.text`` - 1,161,276 bytes - 64.4% of allocated sections * - ``.dynstr`` + ``.dynsym`` + ``.gnu.hash`` - 271,559 bytes - Cost of the large exported ABI * - Unwind and exception tables - 211,113 bytes - ``.eh_frame*`` and ``.gcc_except_table`` * - ``.rodata`` - 40,008 bytes - Not the main contributor The shared object defines approximately 2,119 dynamic symbols. Symbol-name classification finds about 270 parsing, 629 serialization, 179 size, and 30 printing exports. These groups overlap, but they show that generated per-message forwarding methods dominate the public surface. The baseline can be reproduced with: .. code-block:: bash stat --format='%s' onnx_light/onnx_py/liblib_onnx_proto.so size -A -d onnx_light/onnx_py/liblib_onnx_proto.so readelf --dyn-syms -W onnx_light/onnx_py/liblib_onnx_proto.so nm -S --size-sort --demangle \ onnx_light/onnx_py/liblib_onnx_proto.so Why the library is large ++++++++++++++++++++++++ Generated message API ^^^^^^^^^^^^^^^^^^^^^ ``SERIALIZATION_METHOD`` currently adds a broad API to every proto class: * two ``ParseFromString`` overloads; * zero-copy, stream, file-descriptor, array, and iostream parsing; * string, array, stream, file-descriptor, and iostream serialization; * size computation and text printing. Most wrappers perform the same adaptation around a much smaller wire-format core. Because the shared library exports them for every message class, the linker must retain them even with ``-ffunction-sections`` and ``--gc-sections``. Exported ABI ^^^^^^^^^^^^ The target does not use hidden visibility on ELF platforms. Template instantiations, compatibility wrappers, helper methods, and message methods therefore enter ``.dynsym`` and ``.dynstr``. The symbol names are particularly large because many exported functions contain fully expanded C++ template types. Mixed responsibilities ^^^^^^^^^^^^^^^^^^^^^^ ``lib_onnx_proto`` currently includes more than wire parsing and serialization: * model and tensor verification; * advanced external-data rewriting and alignment; * encrypted model I/O and the OpenSSL dependency; * BLAKE3 hashing; * thread-pool support; * general ``onnx_light_helpers`` utilities; * text-format printing. These features are useful, but a parser/serializer-only consumer should not have to load all of them. Compiler-generated metadata ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Exceptions, many out-of-line functions, and the large dynamic ABI generate substantial unwind, exception, relocation, and procedure-linkage tables. Removing source code without reducing the number of retained functions and exports will therefore leave a significant secondary cost. Optional feature splitting ++++++++++++++++++++++++++ Feature splitting is a separate workstream, not part of the primary binary size plan below. It changes target composition and dependency ownership, so its impact must be measured independently from code-generation and linker improvements. The minimal shared library should own only: * message storage and field access needed across shared-library boundaries; * binary stream primitives; * wire parsing and serialization; * ordinary inline and external tensor payload loading; * the minimal public API needed by the Python bindings and dependent onnx-light libraries. Optional responsibilities should move behind separate targets: .. list-table:: :header-rows: 1 :widths: 32 34 34 * - Candidate target - Responsibility - Current sources to examine * - ``lib_onnx_proto`` - Minimal parser, serializer, messages, and streams - ``onnx.cc``, ``stream.cc``, required parts of ``simple_string.cc`` * - ``lib_onnx_proto_tools`` - Verification and advanced external-data transformations - ``onnx_verify.cc``, ``onnx_helper.cc`` * - ``lib_onnx_proto_hash`` - Content hashes - BLAKE3 sources and hash-specific message methods * - ``lib_onnx_proto_crypto`` - Encrypted model I/O - ``onnx_crypt.cc`` and OpenSSL * - ``lib_onnx_proto_text`` - Human-readable proto printing - ``PrintToStringStream`` implementations An ``onnx_proto_full`` interface target may link all components for existing high-level consumers. The minimal target must not acquire optional dependencies transitively. Generated API reduction +++++++++++++++++++++++ The highest-priority structural change is to stop emitting every convenience wrapper for every message. Each message still needs a small type-specific core: .. code-block:: text ParseFromStream(BinaryStream&, ParseOptions&) SerializeToStream(BinaryWriteStream&, SerializeOptions&) const SerializeSize(BinaryWriteStream&, SerializeOptions&) const String, array, iostream, zero-copy, and file-descriptor entry points should be implemented as inline CRTP/mixin wrappers or shared non-template adapters. Only wrappers used by a consumer are then instantiated in that consumer. Text printing should not be part of the mandatory serialization macro. It can be supplied by ``lib_onnx_proto_text`` or enabled explicitly for builds that need protobuf-compatible debug output. This refactoring is an ABI change and must be measured separately from feature splitting. Wire compatibility does not require preserving every out-of-line convenience symbol. This step is now implemented with ``ProtoMessageAdapter``. Generated messages inherit its inline compatibility API, while only ``ParseFromStream``, ``SerializeToStream``, ``SerializeSize``, and text printing remain substantial type-specific out-of-line methods. ``CopyFrom`` keeps a 30-byte type-specific entry point for ABI compatibility, but delegates its serialization/deserialization pipeline to one type-erased shared implementation. The Release build no longer exports one copy of each string, array, iostream, zero-copy, and file-descriptor adapter for every message. On the same local Linux Release configuration used for step 2, the defined dynamic symbol count decreased from 1,191 to 652. The stripped library decreased from 1,625,024 to 1,001,208 bytes, a reduction of 623,816 bytes (about 609 KiB). Visibility and linking ++++++++++++++++++++++ After identifying the cross-library ABI, the ELF and Mach-O builds should use: .. code-block:: cmake set_target_properties(lib_onnx_proto PROPERTIES CXX_VISIBILITY_PRESET hidden VISIBILITY_INLINES_HIDDEN YES) An ``ONNX_LIGHT_PROTO_API`` annotation or linker version script should expose only symbols required by public consumers and other onnx-light shared libraries. Internal templates, helper functions, and implementation details must remain hidden. This step is now implemented for ELF and Mach-O builds. The shared target uses hidden visibility by default, while proto messages, stream types, and the documented helper API are explicitly marked with ``ONNX_LIGHT_PROTO_API``. Windows retains ``WINDOWS_EXPORT_ALL_SYMBOLS`` until explicit DLL import/export annotations are introduced there. On the local Linux Release baseline, the change reduced the defined dynamic symbol count from 2,128 to 1,191. The stripped library decreased from 1,810,208 to 1,625,024 bytes, a reduction of 185,184 bytes (about 181 KiB). The existing function/data sections and dead-section elimination should be retained. Once visibility is reduced, ``--gc-sections`` can discard code that is currently kept alive only because it is exported. Identical code folding may be enabled when supported by the selected linker. Build and packaging improvements ++++++++++++++++++++++++++++++++ Release wheels should strip unneeded static symbols. This is an immediate reduction of approximately 368 KB in the measured build and does not change the runtime ABI. The following build variants may optionally be compared if additional size headroom becomes necessary: * ``Release`` versus ``MinSizeRel``; * ``-O2`` versus ``-Os``; * link-time optimization or ThinLTO; * ``-fno-semantic-interposition`` for hidden internal functions; * linker identical-code folding. These options are secondary to API consolidation and feature splitting. Compiler flags alone cannot remove thousands of intentionally exported functions. Static linking remains useful for a standalone parser because the final linker can retain only referenced sections. Python extensions still need one shared proto implementation so that message objects and RTTI are not duplicated across modules. Measurement plan ++++++++++++++++ Every experiment should record: * unstripped and stripped file sizes; * allocated section sizes; * ``.text``, dynamic symbol/string, relocation, and unwind sizes; * number of defined dynamic symbols; * parse and serialization time on the same representative models; * peak memory while parsing; * required shared-library dependencies. Measurements must use a clean build with a recorded compiler, linker, build type, architecture, ``ONNX_ML`` setting, and OpenSSL setting. Comparing files from different configurations is not actionable. Proposed budgets ++++++++++++++++ For the same Linux x86-64 configuration as the baseline: .. list-table:: :header-rows: 1 :widths: 34 28 38 * - Milestone - Installed size - Required change * - Packaging baseline - At most 1.75 MiB - Strip release artifacts * - Minimal parser/serializer - At most 1.25 MiB - Reduce exports and generated wrappers * - Stretch target - At most 1.00 MiB - Consolidate generated wrappers and enable size-oriented linking The parser and serializer must remain wire-compatible. Performance regressions must be reported alongside size gains rather than hidden by the aggregate binary-size number. Implementation order ++++++++++++++++++++ 1. **Implemented:** strip the installed Release artifact and report its file, section, text, dynamic-symbol, and dependency sizes in CI. See `PR #4333 `_. 2. **Implemented:** introduce hidden visibility and an explicit ``ONNX_LIGHT_PROTO_API`` cross-library export boundary. See `PR #4344 `_. 3. **Implemented:** replace per-message convenience implementations with the inline ``ProtoMessageAdapter`` CRTP adapter. See `PR #4349 `_. 4. **Optional:** compare ``MinSizeRel``, LTO, and linker folding if additional size headroom becomes necessary. 5. **Implemented:** enforce a 1.2 MiB installed-size budget in CI for the Linux x86-64 Release build. See `PR #4355 `_. Outcome by step +++++++++++++++ The following measured results and optional projection use the 2.08 MiB unstripped Linux baseline. They are presented in implementation order; visibility, wrapper consolidation, and garbage collection may eliminate some of the same code. .. list-table:: :header-rows: 1 :widths: 8 28 20 25 19 * - Step - Change - Direct gain - Resulting size - Status * - 1 - Strip Release artifacts - 359 KiB measured - 1.73 MiB - Measured * - 2 - Hide internal symbols and reduce exports - 181 KiB measured - 1.55 MiB - Measured * - 3 - Share or inline per-message convenience wrappers - 609 KiB measured - 0.95 MiB - Measured * - 4 - Optional: ``MinSizeRel``, LTO, and identical-code folding - 50--150 KiB - 0.85--1.15 MiB - Not pursued * - 5 - Enforce the CI budget - No immediate reduction - Prevents regressions - Implemented The optional feature split could save a further 100--300 KiB and remove dependencies such as OpenSSL from parser-only deployments. It is deliberately excluded from the cumulative figures because it changes library composition rather than optimizing the same target. The measured stripped library is now approximately 0.95 MiB after wrapper consolidation, meeting the stretch target without the optional compiler and linker experiments in step 4. CI enforces the 1.2 MiB installed-size budget on the matching Linux x86-64 Release build.