Showing posts with label cpp_linkage. Show all posts
Showing posts with label cpp_linkage. Show all posts

Oct 30, 2026

[linker] link time GC

Reference:
https://vsdmars.blogspot.com/2022/09/elf-entry-function.html
https://vsdmars.blogspot.com/2024/12/ccpponsea-keep-c-binaries-small-summary.html


Link-time GC is the linker throwing away code and data that nothing uses. With 
  • -ffunction-sections 
  • -fdata-sections 
  • and --gc-sections
the compiler puts each function and global in its own section. The linker starts from a set of roots (the entry point main/_start, exported symbols, and every entry in .init_array, which holds the static initializers). It follows references from those roots and drops every section it never reaches.

That's why static initializers get in the way. Each one is a root, so everything it references stays in the binary, even if the program never uses it at runtime:cpp


// registry.cc
static Registrar r("foo", &FooHandler); // the static initializer is in .init_array, so it's a root
// FooHandler and everything it calls stay in the binary,
// even if nothing ever looks up "foo".


Without that global, nobody would reference FooHandler, and the linker would drop it.

Sep 12, 2026

C++ template code COMDAT folding

Reference:
Reducing C++ template bloat by factoring out the type-dependent portions of the function


COMDAT folding (also known as Identical Code Folding or ICF/Safe ICF) is a link-time optimization where the linker detects functions or read-only data sections that compile into byte-for-byte identical machine code, merges them into a single instance, and points all call sites to that unified copy.

When instantiates templates over types that share the same underlying memory layout and operations, the compiler often emits redundant assembly:

template <typename T>
void push_item(T* item) {
    // Operations on item...
}

// In translation unit A:
push_item<Dog>(dog_ptr);

// In translation unit B:
push_item<Cat>(cat_ptr);
Because Dog* and Cat* are both raw machine pointers (typically 8 bytes), the generated machine instructions for push_item and push_item are identical. 

Normally, C++ templates emit code into COMDAT sections (Common Data sections) with "link-once" semantics (select any). This allows the linker to discard duplicates of push_item compiled across multiple .cpp files. However, normal deduplication only matches identical symbol names. It cannot eliminate the identical assembly between push_item and push_item because their mangled symbol names differ.

How COMDAT Folding Works

During the link step, the linker inspects all candidate sections:

Content Comparison: The linker compares the byte streams, relocation targets, and alignment requirements of functions marked as foldable (typically COMDAT sections generated by inline functions, template instantiations, and virtual tables).

Section Merging: If two distinct functions (e.g., std::vector<int*>::size() and std::vector<char*>::size()) produce identical instructions and reference identical offsets, the linker discards one function body.

Symbol Redirection: The symbol table entry for the discarded function is updated to point directly to the entry point of the retained function.

Key Benefits

Reduced Binary Footprint: Prevents template-heavy code (like std::vector<T*> for hundreds of pointer types) from inflating executable size.

Instruction Cache (I-Cache) Efficiency: Multiple logically distinct types share hot cache lines instead of thrashing the instruction cache with duplicate code.


The Function Pointer Quirk

Under strict C++ rules, every distinct function must have a unique address:

assert(&push_item<Dog> != &push_item<Cat>);

When COMDAT folding collapses these functions, &push_item<Dog> == &push_item<Cat> evaluates to true. This can break code that relies on function pointer uniqueness for type-tagging or callback registries.

To handle this, linkers provide different safety levels:
LLVM (lld)  --icf=safe Only merges functions whose addresses are never taken, preserving C++ address-uniqueness guarantees.
LLVM (lld)  --icf=all Aggressively merges all identical functions, even if addresses are taken.


Instead of generating redundant machine code for hundreds of pointer instantiations and hoping the linker cleans it up with COMDAT folding / ICF, standard library implementations and runtime systems use type erasure with non-templated (or void-pointer-based) base classes.

By shifting the heavy procedural logic into a shared base class, the exposed template becomes a thin, inline wrapper that carries zero runtime binary overhead.

The Architecture: Base-Derived Split

The pattern divides a container or utility into two layers:

The Erased Base Class: Implements memory allocation, capacity resizing, buffer shifts, and element index arithmetic using void* or raw byte buffers. This code is compiled once into the runtime library or translation unit.

The Typed Template Wrapper: Derives from or wraps the base class. It only exposes strongly typed interfaces, using zero-cost casts (reinterpret_cast or static_cast) to translate between T* and void*.


// --- Shared Implementation (compiled once into binary/lib) ---
class VectorPtrBase {
protected:
    void** data_ = nullptr;
    size_t size_ = 0;
    size_t capacity_ = 0;

    void grow_and_insert(size_t index, void* element) {
        // All heavy buffer reallocation, index shifting,
        // and boundary checks happen HERE once.
        if (size_ == capacity_) {
            size_t new_cap = capacity_ == 0 ? 8 : capacity_ * 2;
            void** new_data = new void*[new_cap];
            for (size_t i = 0; i < size_; ++i) new_data[i] = data_[i];
            delete[] data_;
            data_ = new_data;
            capacity_ = new_cap;
        }
        data_[index] = element;
        ++size_;
    }

    void* get_element(size_t index) const {
        return data_[index];
    }
};

// --- Thin Typed Wrapper (specialization for any pointer type) ---
template <typename T>
class Vector<T*> : private VectorPtrBase {
public:
    void push_back(T* val) {
        // Zero-cost static cast; inlines down to a direct call to the base
        grow_and_insert(size_, static_cast<void*>(val));
    }

    T* operator[](size_t index) const {
        return static_cast<T*>(get_element(index));
    }

    size_t size() const { return size_; }
};

Why This Beats Relying Purely on COMDAT Folding

Faster Compilation Times: The compiler does not have to parse, instantiate, type-check, and generate intermediate representation (IR) / assembly for grow_and_insert across Vector<Apple*>, Vector<Banana*>, and Vector<Car*>.

Lower Linker Overhead: Linker-time ICF requires analyzing section hashes, inspecting instruction bytes, and walking relocation tables to prove equivalence. Pointer erasure eliminates the duplicate sections before the object files reach the linker.

Guaranteed Code Sharing: COMDAT folding is sensitive to subtle differences (such as debug information emission, compiler optimization levels, or platform-specific pointer calling conventions). The base-class approach enforces code sharing by construction.

Preserved Pointer Address Guarantees: Because the wrapper's member functions inline entirely into the caller or delegate to the shared base, it avoids issues where --icf=safe refuses to fold functions whose addresses were taken.

Dec 1, 2024

[C++][cpponsea] Keep C++ Binaries Small - summary

Reference:
https://youtu.be/7QNtiH5wTAs?si=c8I_prA9k9Hasfdr


Problem

  • Devices with limited storage
  • Devices with limited bandwith
  • Env impact

Parts of an executable

  • Header
  • Text
  • Data (init. vs uninit)
  • Read-only data
  • symbol table
  • Relocation info
  • (ref https://eli.thegreenplace.net/2013/07/09/library-order-in-static-linking)
  • Import table / Procedure linkage table
  • Export table
  • Exception handling info
  • Debugging info

Consider

  • cross platform?
  • linking/symbol resolution handling
  • PIC
  • Endianness
  • Architecture / extensibility

Tools


Coding part


Object init.

0(.zerofill) vs. other thing else; decrease size


Member ordering

Usually won't reduce the binary size due to compiler is able to optimize.


Special member functions

  • inline default special functions
  • inline empty special functions
  • following the rule of zero(RoZ)
  • with either virtual or non-virtual destructor


templates

  • minimal template code section; same as marco
  • Exact/promote to the upper level, which the code is not generated
    for every new type instance.

Compiler options

  • -Os
  • -flto
  • -fdata-sections / -ffunction-sections -Wl, --gc-sections
    • The Traditional Way (Still Valid) If you are linking against a traditional static library (.a file), the linker still operates on an object-file level. If a static library is built with strlen.o, strcpy.o, and printf.o as separate files inside the archive, and your code only references strlen, the linker will strictly pull in strlen.o. The other object files are completely ignored, keeping your binary small. Many embedded and legacy C libraries are still structured exactly this way.
    • The Modern Way: Section Garbage Collection Today, instead of splitting source code into a million files, we let the compiler and linker do the heavy lifting using function-level sections. When compiling modern C code (using GCC or Clang), you can pass specific flags: -ffunction-sections and -fdata-sections: This tells the compiler to put every single function and data item into its own distinct section inside a single object file, rather than bundling them all into one giant .text section. --gc-sections (Linker flag): This tells the linker to perform "garbage collection" and throw away any unused sections during the final build. Why this matters: You can have a single string.c file containing 50 functions. With these flags, the linker will still grab only strlen and discard the other 49, achieving the exact same tiny binary size without the architectural headache.
    • Link-Time Optimization (LTO) The ultimate evolution of this is LTO (compiled with -flto). With LTO enabled, the compiler doesn't just emit machine code into object files; it emits its internal intermediate representation. At the linking stage, the compiler looks at the entire program holistically. It can inline strlen directly into your code, eliminate dead code with extreme precision, and completely optimize away unused functions, regardless of how the source files or libraries were structured.
  • -Wl, --icf=all or -Wl, --icf==safe
  • -Wl, -s and -Wl, --strip-all
  • -Wl, --as-needed
  • -mllvm -inline-threshold=&lt;n>
  • -fstack-protector
    https://clang.llvm.org/docs/ClangCommandLineReference.html#cmdoption-clang-fstack-protector
  • -finline-limit=n
    https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html#index-finline-limit
  • Identical Code Folding (ICF)
    https://research.google/pubs/safe-icf-pointer-safe-and-unwinding-aware-identical-code-folding-in-gold

Nov 21, 2024

[C++] key function

Reference:
undefined reference to vtable for X
What is a C++ "Key Function" as described by gold?



Every polymorphic class requires a virtual table or vtable describing the type and its virtual functions. Whenever possible the vtable will only be generated and output in a single object file rather than generating the vtable in every object file which refers to the class (which might be hundreds of objects that include a header but don't actually use the class.)

The cross-platform C++ ABI states that the vtable will be output in the object file that contains the definition of the key function, which is defined as the first non-inline, non-pure, virtual function declared in the class.

If you do not provide a definition for the key function (or fail to link to the file providing the definition) then the linker will not be able to find the class' vtable.


class X {
public:
  virtual ~X() = 0;
  void f();
  virtual void g() { }
  virtual void h(); // defined inline out side of class.
  virtual void i(); // <-- Key fuction.
  virtual void j();
};

inline void X::h() { }

reproduce the error:

#include <iostream>

class Base {
public:
    virtual void foo() = 0; // Pure virtual function
};

class Derived : public Base {
public:
    void foo() override; // Declaring but not defining foo()
};

int main() {
    Derived d; // This will cause a linker error due to missing vtable
    return 0;
}