One of the many enhancements coming to libstdc++ shipped in GCC 13, which is expected to be released in May 2023, addresses an old pain point of the <iostream>
header. In current versions of libstdc++, including <iostream>
in a translation unit (TU) introduces a global constructor into the compiled object file, one that is responsible for initializing the standard stream objects std::cout
, std::cin
, etc., on program startup.
In libstdc++ for GCC 13, this will be no more, as we’ve moved the initialization of the standard stream objects into the shared library. The benefit of this is reduced executable size, improved link times, and improved startup times for C++ programs that make heavy use of <iostream>
.
An example
Consider the canonical C++ Hello World program:
#include <iostream>
int main() {
std::cout << "Hello world!\n";
}
At some point in your journey as a C++ programmer, you've probably compiled this program and inspected the resulting assembly. Using GCC 12.2 as the compiler yields the following assembly:
.LC0:
.string "Hello world!\n"
main:
subq $8, %rsp
movl $.LC0, %esi
movl $_ZSt4cout, %edi # std::cout
call std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
movl $0, %eax
addq $8, %rsp
ret
_GLOBAL__sub_I_main:
subq $8, %rsp
movl $_ZStL8__ioinit, %edi # std::__ioinit
call std::ios_base::Init::Init() [complete object constructor]
movl $__dso_handle, %edx
movl $_ZStL8__ioinit, %esi # std::__ioinit
movl $_ZNSt8ios_base4InitD1Ev, %edi # std::ios_base::Init::~Init()
call __cxa_atexit
addq $8, %rsp
ret
This is a surprising amount of assembly code for Hello World. Notably, alongside the expected main
definition which invokes the appropriate operator<<
overload, somehow a global initializer (the _GLOBAL__sub_I_main
symbol) snuck in, one which seems to construct an object __ioinit
of type std::ios_base::Init
(and schedules destruction of the object at program exit). One might wonder, how did that get there and what is its purpose?
In contrast, if we compile with GCC trunk, we instead get the following:
.LC0:
.string "Hello world!\n"
main:
subq $8, %rsp
movl $.LC0, %esi
movl $_ZSt4cout, %edi
call std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
movl $0, %eax
addq $8, %rsp
ret
This notably emits no such global initializer (and is much more in line with what one would expect the resulting assembly to be). Instead, an equivalent initializer is present in libstdc++.so
and will run upon dynamic loading of the library (we’ll touch upon the details in a later section).
Why the __ioinit object
The global initializer we see in the GCC 12.2 output corresponds to the static global object __ioinit
that's defined in <iostream>
:
// For construction of filebuffers for cout, cin, cerr, clog et. al.
static ios_base::Init __ioinit;
As the comment above it suggests, the purpose of this object is ultimately to ensure that the standard stream objects std::cout
, std::cin
, etc., are properly initialized (via the ios_base::Init::Init
constructor) before they are used in the program by either main()
proper or earlier during the initialization of another global object.
Thus, including <iostream>
implicitly defines a static global object of type ios_base::Init
within the TU. This approach gets the job done because C++ guarantees global objects within one TU are initialized in the order in which they're defined, so we can ensure that the stream objects are usable even during the startup (and shutdown) phase of a program, e.g.:
#include <iostream>
struct A {
A() { std::cout << "A::A()\n"; }
~A() { std::cout << "A::~A()\n"; }
};
static A a; // Works because __ioinit is defined within the TU first
A tempting and more obvious alternative might be to perform initialization of the standard stream objects in the stream objects' constructors themselves (which effectively must be defined in the compiled-in part of the library – libstdc++.so
or libstdc++.a
-- rather than in a header because std::cout,
et al. are not inline
objects). But this won't work because C++ gives no guarantees about global object initialization order across TUs, and neither do linkers by default give similar guarantees across object files.
While the static global approach works, there's a significant drawback: a distinct global object will be defined for every TU that includes <iostream>
and will persist all the way into the final executable. Thus at program startup, the constructor (and destructor) for ios_base::Init
will redundantly run once for every constituent TU that includes <iostream>
, leading to code bloat and slower link and startup times.
A better approach
In GCC 13, we essentially moved the __ioinit
definition:
static ios_base::Init __ioinit;
from the <iostream>
header and into the compiled library sources. This was carefully done with the help of the non-standard init_priority
attribute, which gives more control over inter-TU object initialization order than what standard C++ provides. As a result, TUs that include <iostream>
are no longer encumbered by this "invisible" global __ioinit
object.
As you might expect, this change is backward-compatible with programs compiled against earlier versions of libstdc++ because changing the <iostream>
header won’t affect TUs that were compiled against the older <iostream>
header, and the initialization that is now also performed within the compiled library is idempotent.
Most modern platforms support the init_priority
attribute; on platforms that lack such support, we fall back to the old way of defining __ioinit
in <iostream>
.
Conclusion
In GCC 13's libstdc++, we've revisited an age-old implementation decision of how the standard stream objects get defined. The new approach is better in many ways, and it is more in line with the zero-cost abstraction philosophy of C++. This is one of many enhancements to look forward to in the upcoming release of GCC 13; a more complete list of changes can be found at gnu.org.
Last updated: August 14, 2023