The gold Linker for Edge Agent Binaries: How It Works and When to Use It

How the gold linker works for C++ edge agent runtimes, when it beats GNU ld, and how --gc-sections and ICF affect shipped ELF binary size.

The gold Linker for Edge Agent Binaries: How It Works and When to Use It
Written by TechnoLynx Published on 11 Jul 2026

Ship the on-device portion of an agent as a native C++ runtime and two numbers you rarely thought about start to matter: how long the link step takes in CI, and how many megabytes of un-stripped symbols survive into the final ELF binary. On a phone-class target, both are shipping constraints, not build-system trivia. The linker is the tool that decides them, and for large ELF binaries on Linux and Android, the default your toolchain reaches for is often GNU ld — a choice most teams never actually make, they simply inherit.

That is the mistake worth naming up front. The linker is treated as invisible plumbing: whatever gcc or the NDK hands you is what you use, and its effect on binary size or cold start never gets measured. gold exists precisely because that default was slow and coarse for the exact workload edge agent runtimes produce — big C++ objects, heavy template instantiation, and a hard memory ceiling on the device. Choosing it, or knowingly not choosing it, is one lever inside the runtime-footprint work that governs whether a native agent fits on the device at all.

How does the gold linker work?

gold is an ELF-only linker originally written to replace GNU ld for large C++ programs. The important part is not that it is a different program but that it makes different decisions about the same job. A linker takes a pile of object files and libraries, resolves symbols, lays out sections, discards what nothing references, and emits an executable or shared object. gold was designed for that pipeline to run faster on large inputs and to be more aggressive and predictable about what it throws away.

Two behaviours matter for an edge agent binary. The first is section garbage collection (--gc-sections), which drops sections no reachable symbol references — dead code and unused data that would otherwise be linked in and shipped. The second is identical code folding (ICF, --icf), which merges functions with byte-identical machine code into a single copy. C++ with heavy templating produces a lot of near-duplicate instantiations, and folding them can meaningfully shrink the text segment. Neither behaviour is unique to gold in principle, but gold implemented them early and treats them as first-class, which is why the linker choice shows up in the size and startup numbers rather than staying invisible.

In practice, “using gold” usually means passing -fuse-ld=gold to the compiler driver so it invokes gold instead of ld, then confirming your --gc-sections and -ffunction-sections/-fdata-sections compile flags are set so the linker actually has fine-grained sections to collect. The compile-side half of that pairing — why -ffunction-sections has to be on for --gc-sections to do anything — is covered in our walkthrough of what each GCC flag actually does for edge inference builds. The linker and the compiler flags are one system; changing one without the other produces confusing results.

The honest answer is: not always, and not by a fixed amount. The gains concentrate in a specific shape of workload — large C++ binaries with many object files, template-heavy code, and a build loop where engineers relink repeatedly during iteration. That is exactly the shape a native on-device agent runtime tends to have, which is why the topic is worth a hub of its own rather than a footnote.

The mechanism behind the link-time gain is that gold was built for faster linking of large ELF inputs, and its incremental behaviour reduces the wall-clock cost of the relink step that dominates a tight edit-compile-link loop. The size gain comes from --gc-sections and ICF removing code that ld, with less aggressive defaults, would carry through. But if your binary is small, mostly C, or already linked with an aggressive modern configuration, the delta narrows to nothing — and on very large link jobs, lld (the LLVM linker) now often out-links gold on raw speed. Treat the numbers below as observed patterns from build work on native runtimes, not a benchmark you can quote as a fixed rate.

When gold moves the needle — and when it doesn’t

Situation gold vs GNU ld What to check first
Large C++ agent runtime, template-heavy, frequent relinks Faster link, smaller ELF with --gc-sections + ICF -ffunction-sections is set at compile time
Small or mostly-C binary Little to no difference Whether linker choice is even worth measuring
Already using an aggressive modern link config Marginal size delta Whether lld is the better modern target
Very large link job, speed is the bottleneck lld may out-link gold Benchmark both against your CI clock
CoreML / browser / non-ELF target Not applicable The target’s own toolchain, not gold

The one row worth internalising is the last: gold is an ELF/Linux and Android tool. It does not govern CoreML, WebAssembly, or browser inference targets, which use their own linkers and packaging. If your edge story spans multiple targets, gold is the answer for exactly one of them.

How do –gc-sections and identical code folding affect the dead code in a shipped binary?

This is where the abstraction meets the memory ceiling. --gc-sections works at the granularity of sections, so it can only drop what the compiler emitted as a separately-collectable unit — which is why -ffunction-sections -fdata-sections at compile time is the precondition. With those on, the linker builds a reachability graph from your entry points and roots, and any function or data section nothing reaches is discarded before it reaches the output ELF. For an agent runtime that links a large inference library but calls a narrow slice of it, this is often the single largest source of avoidable binary weight.

Identical code folding attacks a different problem. Template instantiation and code generation produce many functions whose machine code is byte-for-byte identical despite different source. ICF collapses them to one copy. The saving depends entirely on how much duplication your codebase actually contains — a heavily generic C++ runtime can see a real text-segment reduction, while a lean codebase sees almost nothing. There is a correctness caveat worth stating plainly: aggressive ICF can fold functions whose addresses you assumed were distinct, so if code relies on function-pointer identity, use the safer folding mode and test.

The point that ties both together: linker-level dead-code elimination is complementary to, not a substitute for, model-level footprint work. Stripping symbols and folding code shrinks the runtime; quantisation and distillation shrink the model weights that ship alongside it. Both compound against the same device memory ceiling. Our discussion of distillation, quantisation, and runtime fit for edge inference covers the model half of that budget, and the two decisions should be planned together rather than in separate build and modelling silos.

Which parts of the runtime-footprint axis does linker choice actually move?

Being precise about the boundary matters, because linker choice is easy to over-credit. It moves two things directly: the link-step wall-clock time in CI, and the stripped binary size of the native runtime. Through smaller binary size and better section layout it can influence cold-start time, though startup is dominated by other factors — page faults, dynamic-loader work, initialisation of large static structures, model load.

It does not move the model weights, the runtime’s steady-state memory during inference, or the accuracy of the model. It does not fix a runtime that is large because it genuinely does a lot of work. Linker choice is a one-time structural saving on the artifact you ship; it cannot substitute for architectural decisions about what the on-device runtime should contain. This scoping — treating linker choice as one measurable input to the runtime-footprint portion of a broader edge-aware assessment, alongside quantisation, kernel selection, and dependency pruning — is the same framing we apply across edge-constrained agent inference work, where the on-device budget is set by the hardware, not by preference.

Is gold still the right choice, or should you be evaluating lld?

This is the live question, and pretending otherwise would date the article badly. gold was the answer to ld’s slowness for a decade. lld, the LLVM project’s linker, is now frequently faster than gold on large link jobs and is the default in several toolchains, including the modern Android NDK path. If you are choosing a linker for a new native edge agent runtime today, lld deserves to be in the comparison, not assumed away because gold was the historical upgrade.

The practical framing: gold versus ld is a settled improvement for the large-C++ ELF case; gold versus lld is a genuine measurement question that depends on your codebase, your CI hardware, and which toolchain you are already committed to. Do not treat “use gold” as a permanent recommendation. Treat it as “measure your link step and your stripped binary, and pick the ELF linker that wins on your numbers.” The decision should be evidence-led, the same way the broader generative AI engineering work treats every footprint lever — measured against the device, not chosen by reputation.

How do you measure the effect on cold start and stripped binary size against a phone-class ceiling?

Measurement is the whole point, because the gains are workload-specific. A minimal, honest procedure:

  1. Fix the compile flags. Ensure -ffunction-sections -fdata-sections are set so --gc-sections has something to work with. Changing the linker without these makes the comparison meaningless.
  2. Measure link wall-clock in CI. Time the link step alone (not the full build) across several relinks for ld, gold, and lld. Iteration cost lives here.
  3. Measure the stripped binary. Run strip and compare the on-disk size of the shipped ELF for each linker, with and without --gc-sections and ICF. Compare against the device’s memory ceiling, not against each other in the abstract.
  4. Measure cold start on-device. Time process start to first-inference-ready on the actual phone-class target, repeated, cold. Attribute cautiously — startup has many contributors beyond binary layout.
  5. Record the deltas as your own numbers. These are observed patterns for your codebase, not portable benchmarks; the next codebase may behave differently.

That procedure is deliberately boring. The value of gold — or lld, or leaving ld in place — is only knowable against your artifact and your ceiling, and the measurement outlives any advice about which linker to prefer.

Where does linker choice not matter?

It is worth closing the boundary explicitly. Linker choice is irrelevant where you are not producing an ELF binary. A CoreML model deployed to iOS goes through Apple’s toolchain and packaging; a browser inference target compiled to WebAssembly uses its own linker and has entirely different size mechanics; a managed-runtime deployment never sees gold at all. On those paths, arguing about --gc-sections is arguing about a tool that is not in the build. The compression decisions that do travel across all of them — quantisation, distillation, and format choice — are where cross-platform footprint effort belongs, and where the compounding gains with linker-level savings on the ELF path show up. For the cross-platform runtime picture, our guide to OpenCL for cross-platform edge inference covers the portability layer that sits underneath these decisions.

FAQ

What matters most about the gold linker in practice?

gold is an ELF-only linker built to replace GNU ld for large C++ programs, making faster and more aggressive decisions about the same linking job. In practice you enable it with -fuse-ld=gold and pair it with -ffunction-sections -fdata-sections at compile time so its --gc-sections dead-code removal actually has fine-grained sections to collect. The point is that the linker is a decision, not invisible plumbing.

The gains concentrate in large, template-heavy C++ binaries with frequent relinks — exactly the shape a native edge agent runtime tends to have. Faster relinking comes from gold’s design for large ELF inputs; smaller binaries come from --gc-sections and identical code folding removing code ld would carry. On small, mostly-C, or already-aggressively-configured binaries the delta narrows to nothing, so measure before assuming.

How do gold’s –gc-sections and identical-code-folding affect the dead code carried into a shipped edge binary?

--gc-sections builds a reachability graph and discards any function or data section nothing references, which is often the largest source of avoidable weight when a runtime links a big library but uses a narrow slice. Identical code folding merges byte-identical function copies, helping most where template instantiation produces duplication. Aggressive ICF can break code relying on function-pointer identity, so use a safe folding mode and test.

Which parts of the parent hub’s runtime-footprint axis does linker choice actually move, and which it cannot?

Linker choice directly moves link-step wall-clock time in CI and stripped binary size, and indirectly influences cold-start time through smaller, better-laid-out binaries. It does not move model weights, steady-state inference memory, or model accuracy, and it cannot fix a runtime that is large because it genuinely does a lot of work. It is a one-time structural saving on the shipped artifact, complementary to quantisation and distillation.

Is gold still the right choice, or should I be evaluating lld for edge C++ agent runtimes?

gold versus ld is a settled improvement for the large-C++ ELF case, but gold versus lld is a live measurement question. lld is now frequently faster on large link jobs and is the default in several modern toolchains including the Android NDK path. For a new runtime, put lld in the comparison and pick the ELF linker that wins on your CI clock and stripped-size numbers.

How do I measure the effect of linker choice on cold-start time and stripped binary size against a phone-class memory ceiling?

Fix -ffunction-sections -fdata-sections first, then time the link step alone across several relinks for ld, gold, and lld; strip and compare on-disk ELF size with and without --gc-sections/ICF against the device ceiling; and time cold process-start-to-first-inference on the actual target, attributing cautiously since startup has many contributors. Record the deltas as your own observed numbers, not portable benchmarks.

Where does linker choice not matter — for example on CoreML or browser inference targets?

Linker choice is irrelevant wherever you are not producing an ELF binary: CoreML on iOS uses Apple’s toolchain and packaging, WebAssembly browser targets use their own linker with different size mechanics, and managed runtimes never invoke gold. On those paths the cross-platform footprint effort belongs to quantisation, distillation, and format choice — the compression decisions that travel across every target.

The linker is one input to the edge-aware runtime-footprint assessment, not the whole of it. If a native agent is missing its memory ceiling, the failure to name is un-measured artifact weight — dead code and un-stripped symbols shipping because nobody timed the link step or stripped the ELF against the device budget.

Back See Blogs
arrow icon