Trying out snmalloc on Macos I wondered why all the created binaries are >256MiB.
It turns out that zero-initialized static inline
data members are lowered in a weird way on Mac OS X, on both ARM64 and x86_64. Even this simple test produces huge binaries:
container.h
#pragma once
#include <cstdint>
class Container {
public:
inline static uint8_t inner[256000000];
};
main.cc
#include "container.h"
int main() {
return Container::inner[0];
}
Compiled like this:
$ ~/clang+llvm-12.0.0-x86_64-apple-darwin/bin/clang -O3 -std=c++17 main.cc --target=x86_64-apple-darwin -c; ls -l main.o
-rw-r--r-- 1 hans staff 256000744 Jun 21 16:29 main.o
It is the same with open-source clang as with Apple clang. gcc behaves similarly.
On Linux (compiled with either clang or gcc) it is included in the .bss section, thus not taking up any space.
Why is this the case on Macos? And is this a bug or expected behavior?
The . bss section is used by the compiler for global and static variables. It is one of the default COFF sections that is used to reserve a specified amount of space in the memory map that can later be used for storing data. It is normally uninitialized.
In computer programming, the block starting symbol (abbreviated to . bss or bss) is the portion of an object file, executable, or assembly language code that contains statically allocated variables that are declared but have not been assigned a value yet. It is often referred to as the "bss section" or "bss segment".
bss is reserved and has special effects, in particular it occupies no file space, thus the advantage over . data . The downside is of course that all bytes must be set to 0 when the OS puts them on memory, which is more restrictive, but a common use case, and works fine for uninitialized variables.
In computing, a data segment (often denoted . data) is a portion of an object file or the corresponding address space of a program that contains initialized static variables, that is, global variables and static local variables.
So the constructor of the class can’t initialize something which isn’t exclusive to the object of that class ( static data members in this case). That’s why we initialize the Static data member outside the class and not in the constructor. Classically, static variables would all be in the data or bss sections of your program.
"This feature is not provided" because in-class initialization for non-static and static members are semantically very different features. Your suprize is based on the fact that they look superficially similar. But in reality they actually have nothing in common.
The reason for this is simple, static members are only declared in a class declaration, not defined. They must be explicitly defined outside the class using the scope resolution operator .
In-class declaration of a static data member is just that - a declaration. It does not provide definition for that member. The definition has to be provided separately. The placement of that definition in the code of your program will have consequences.
I'll go ahead and take a stab at answering this, though I'll be the first to admit that you can only go so far with an answer before you run into a wall that says "because someone made a decision and you're stuck with it forever."
The primary key to all of this comes in the form of the Mach-O Runtime specification for MacOS, which defines the .bss
section as being used for:
uninitialized static variables (for example,
static int i;
).
You can read about it in this archived version from version 10.3, but you can also find the same information in other Mach-O references.
The important thing to note here is that the use of bss
refers to "private" symbols only. In other words, this refers to a C-style use of the static
keyword, which is guaranteed to be local to the translation unit.
When you declare a C++17 member variable as static inline
, despite the use of the perversely overloaded static
keyword, you've created a global object, of which there is guaranteed to only ever be one instance in a program. In other words, every translation unit compiled with this declaration will instantiate it, and the linker will be expected to "coalesce" them into a single instance by picking one of them. This is obviously quite different from the C-style "uninitialized static variable."
MacOS host compilers like clang implement this by declaring the symbol as weak
DATA
, similar for example to how default constructors would be declared (though those would of course be in TEXT
).
To illustrate this point, note that you could get the same effect without C++17 at all. For example compile these sets of examples this and look at the assembly output:
static uint8_t stuff[256000000]; // <- goes into .bss
int main() {
return (int)reinterpret_cast<uint64_t>(&stuff[0]);
}
Note that I'm having to do the &stuff
thing here to make sure the compiler doesn't optimize away stuff
entirely in this case.
Now try this:
uint8_t stuff[256000000]; // <-- goes into __DATA,__common
int main() {
return (int)reinterpret_cast<uint64_t>(&stuff[0]);
}
Getting closer. Note that stuff
is not put into .bss
like you might see on a linux platform. According again to the Mach-O runtime spec, the common
section is used for:
Uninitialized imported symbol definitions (for example,
int i;
) located in the global scope (outside of a function declaration)."
Now try this:
__attribute__((weak)) uint8_t stuff[256000000]; // <-- in DATA,__data
int main() {
return (int)reinterpret_cast<uint64_t>(&stuff[0]);
}
This is exactly how a static inline
C++17 member variable will be defined. Deep under the hood, clang has assigned this symbol to be "coalesced" data, which on x86 just turns into standard DATA. If you really want to dive into the sausage factory, you can actually see that in the llvm SelectSectionForGlobal function.
if (GO->isWeakForLinker()) {
if (Kind.isReadOnly())
return ConstTextCoalSection;
if (Kind.isReadOnlyWithRel())
return ConstDataCoalSection;
return DataCoalSection;
}
And DataCoalSection
is correspondingly defined here to be identical to the ordinary data section on everything but power PC.
So from my perspective the behavior you're seeing is working as I would expect given the available specifications for the Mach-O runtime.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With