How to reduce the size of the executable?

Viewed 1007

When I compile this code using the {fmt} lib, the executable size becomes 255 KiB whereas by using only iostream header it becomes 65 KiB (using GCC v11.2).

time_measure.cpp

#include <iostream>
#include "core.h"
#include <string_view>

int main( )
{
    // std::cout << std::string_view( "Oh hi!" );
    fmt::print( "{}", std::string_view( "Oh hi!" ) );

    return 0;
}

Here is my build command:

g++ -std=c++20 -Wall -O3 -DNDEBUG time_measure.cpp -I include format.cc -o runtime_measure.exe

Isn't the {fmt} library supposed to be lightweight compared to iostream? Or maybe I'm doing something wrong?

Edit: By adding -s to the command in order to remove all symbol table and relocation information from the executable, it becomes 156 KiB. But still ~2.5X more than the iostream version.

3 Answers

As with any other library there is a fixed cost and a per-call cost. The fixed cost for the {fmt} library is indeed around 100-150k without debug info (it depends on the compiler flags). In your example you are comparing this fixed cost of linking with the library and the reason why iostreams appears to be smaller is because it is included in the standard library itself which is linked dynamically and not counted to the binary size of the executable.

Note that a large part of this size comes from floating-point formatting functionality which doesn't even exist in iostreams (shortest round-trip representation).

If you want to compare per-call binary size which is more important for real-world code with large number of formatting function calls, you can look at object files or generated assembly. For example:

#include <fmt/core.h>

int main() {
  fmt::print("Oh hi!");
}

generates (https://godbolt.org/z/qWTKEMqoG)

.LC0:
        .string "Oh hi!"
main:
        sub     rsp, 24
        pxor    xmm0, xmm0
        xor     edx, edx
        mov     edi, OFFSET FLAT:.LC0
        mov     rcx, rsp
        mov     esi, 6
        movaps  XMMWORD PTR [rsp], xmm0
        call    fmt::v8::vprint(fmt::v8::basic_string_view<char>, fmt::v8::basic_format_args<fmt::v8::basic_format_context<fmt::v8::appender, char> >)
        xor     eax, eax
        add     rsp, 24
        ret

while

#include <iostream>

int main() {
  std::cout << "Oh hi!";
}

generates (https://godbolt.org/z/frarWvzhP)

.LC0:
        .string "Oh hi!"
main:
        sub     rsp, 8
        mov     edx, 6
        mov     esi, OFFSET FLAT:.LC0
        mov     edi, OFFSET FLAT:_ZSt4cout
        call    std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
        xor     eax, eax
        add     rsp, 8
        ret
_GLOBAL__sub_I_main:
        sub     rsp, 8
        mov     edi, OFFSET FLAT:_ZStL8__ioinit
        call    std::ios_base::Init::Init() [complete object constructor]
        mov     edx, OFFSET FLAT:__dso_handle
        mov     esi, OFFSET FLAT:_ZStL8__ioinit
        mov     edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev
        add     rsp, 8
        jmp     __cxa_atexit

Other than static initialization for cout there is not much difference because there is virtually no formatting here, so it's just one function call in both cases. Once you add formatting you'll quickly see the benefits of {fmt}, see e.g. http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p0645r10.html#BinaryCode.

You forget that iostreams is already included in the stdlibc++.so that is not counted towards the binary size since it is a shared library (usually). I believe by default fmt is built as a static library file so it increases the binary size. You need to compile fmt as a shared library with -DBUILD_SHARED_LIBS=TRUE as explained in the building instructions

in your build/link command, why don't you use -Os option (Optimize for size) ?

Related