static const in header cost: dyld ships with redundant 2KB data copy
A subtle but costly issue exists in Apple's dyld (dynamic linker) codebase where the sVersionMap symbol is duplicated across multiple compilation units. This seemingly minor problem demonstrates a common C++ pitfall that leads to binary bloat and will worsen over time.
Background
I identified this issue when I tried to maintain a community fork of Apple's AvailabilityVersions repository.
See more information about the repository here: AvailabilityVersions Fork
The Problem
The sVersionMap is defined in VersionMap.h as a static constant array:
namespace dyld3 {
struct VersionSetEntry {
uint32_t set = 0;
uint32_t macos = 0;
uint32_t ios = 0;
uint32_t watchos = 0;
uint32_t tvos = 0;
uint32_t bridgeos = 0;
uint32_t driverkit = 0;
uint32_t visionos = 0;
bool operator<(const uint32_t& other) const {
return set < other;
}
};
static const std::array<VersionSetEntry, 54> sVersionMap = {{
{ .set = 0x007db0901, .ios = 0x00050000, .macos = 0x000a0700 },
{ .set = 0x007dc0901, .ios = 0x00060000, .macos = 0x000a0800 },
// ... 52 more entries
}};
}
This header is included in multiple source files:
DyldAPIs.cpp
(line 46)DyldProcessConfig.cpp
(line 31)
When a static const variable is defined in a header file and included by multiple compilation units, each unit gets its own copy of the variable. This causes multiple copies in the final binary:
__ZN5dyld3L11sVersionMapE__ZN5dyld3L11sVersionMapE_180155140
These are not "true" duplicates that would cause linker errors - they're distinct symbols, each containing an identical copy of the data.
The Cost
Each VersionSetEntry contains:
- (8 fields + 2 padding fields) × 4 bytes = 40 bytes per entry
- 54 entries × 40 bytes = 2,160 bytes per copy
With two copies, the current waste is approximately 2.1 KB. However, this will grow as:
- More
VersionSetEntryrecords will be added over time
- More source files might potientially include
VersionMap.h
The Solution
There are several ways to fix this multiple copies issue:
Option 1: Use inline constexpr (C++17 or later) - Recommended
VersionMap.h:
namespace dyld3 {
inline constexpr std::array<VersionSetEntry, 54> sVersionMap = {{
{ .set = 0x007db0901, .ios = 0x00050000, .macos = 0x000a0700 },
{ .set = 0x007dc0901, .ios = 0x00060000, .macos = 0x000a0800 },
// ... all entries
}};
}
This approach ensures:
Single definition: The linker will merge all instances into a single symbol
Compile-time constant:constexprmakes it a compile-time constant (better thanconst)
No ODR violations:inlineallows the definition in the header without One Definition Rule violations
No code bloat: Only one copy will exist in the final binary
Option 2: Use extern declaration + single definition (C++14 compatible)
VersionMap.h:
namespace dyld3 {
extern const std::array<VersionSetEntry, 54> sVersionMap;
}
VersionMap.cpp:
#include "VersionMap.h"
namespace dyld3 {
const std::array<VersionSetEntry, 54> sVersionMap = {{
{ .set = 0x007db0901, .ios = 0x00050000, .macos = 0x000a0700 },
// ... all entries
}};
}
Recommended Approach for dyld
The inline constexpr approach (Option 1) is the cleanest and most modern solution. Considering the current architecture design and legacy compatibility requirements, Option 1 is the most appropriate fix from my perspective.
For this specific case, the fix can be implemented by modifying line 253 in availability.py from the AvailabilityVersions repository:
- static const std::array<VersionSetEntry, {}> sVersionMap = {{{{
+ inline constexpr std::array<VersionSetEntry, {}> sVersionMap = {{{{
This patch maintains backward compatibility while leveraging modern C++ features that dyld already supports with its C++20 configuration.
Binary Impact Analysis
Before the fix, the __const section contains multiple copies:
; Section __const
; Range: [0x100000b80; 0x100001900[ (3456 bytes)
; File offset : [2944; 6400[ (3456 bytes)
__ZN5dyld3L11sVersionMapE: // dyld3::sVersionMap
...
__ZN5dyld3L11sVersionMapE_100001240_1: // Second copy
...
After applying the fix, the __const section is reduced by exactly half:
; Section __const
; Range: [0x100000b80; 0x100001240[ (1728 bytes)
; File offset : [2944; 4672[ (1728 bytes)
__ZN5dyld311sVersionMapE: // dyld3::sVersionMap (single copy)
This demonstrates a concrete 1,728 byte reduction in the binary size, confirming our theoretical calculation.
Status and Apple Feedback
This issue has been reported to Apple via Feedback FB20820097. As of the time of writing, there has been no update or response from Apple regarding this binary optimization opportunity.
Why This Matters
While 2.1 KB may seem negligible currently, this issue becomes significant considering:
- dyld is loaded into every process on macOS/iOS
- The version map will continue growing with new OS releases
- More cpp files in dyld may need to include this header
- Resource-constrained embedded systems require optimal memory usage
Key Takeaway
Always be cautious when implementing code directly in header files, unless you know exactly what you're doing. While header-only approaches offer convenience, they can lead to unexpected consequences like symbol duplication, increased binary size, and compilation overhead. This practice becomes increasingly important as codebases scale and binary size constraints tighten.
For more details on this pattern, see this demonstration repository which illustrates the compilation behavior of static variables in headers.