Featured image for: Value range propagation in GCC with Project Ranger.

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