GCC Binaries sind viel zu groß

Schon 2007 am Anfang meiner C++ Liebschaft wunderte ich mich stets darüber, dass Programme, die aus den gleichen Sourcen kompiliert wurden, unter Linux mit GCC Umgebung um einiges größer waren als unter Windows mit MSVC.

Und ja, das manifestiert sich vor allem im Einsatz von statisch kompilierten Bibliotheken.
Also was kann man dagegen tun?


Windows EXE und Linux Binaries im Vergleich

Mit heutigem Stichtag sehen das größte und kleinste GATE Demo Projekt so aus:

Programm Plattform Größe
gatecli.exe Windows x64 1 890 KB
gatecli Linux x64 2 765 KB
vtxtedit.exe Windows x64 287 KB
vtxtedit Linux x64 1 005 KB

Die statisch kompilierten Bibliotheken sind auf beiden Seiten gleich (zB. zlib, libjpeg oder libreSSL) und trotzdem ergeben sich Größenunterschiede vom Faktor 2 bis 3 zwischen den resultierenden Binärdateien.

Natürlich könnte man argumentieren, dass die statisch einkomilierte C Runtime in Windows nur ein kleiner Wrapper zur dynamischen System-C-Runtime ist, während im GCC größere Funktionssammlungen in die Binärdatei einfließen.
Aber mal im ernst … wenn memcopy() printf(), fopen() und ihre Geschwister eine 1 Megabytes große Implementierung haben, müsste Linux schon verdammt ineffizient sein, wenn andere Implementierungen am Mikrokontroller in wenigen Kilobytes unterkommen.

Einen Unterschied gibt es bei den UI-Tools wie vtxtedit, das unter Windows direkt die WinAPI (kernel, user32 und gdi32) nutzt, während das Linux-Kompilat auf GTK+ 3 zugreift. GTK ist zwar dynamisch gelinkt, was aber nicht heißt, dass hier zusätzliche Dispatcher und Inline-Funktionen zur Geltung kommen.

gatecli hingegen nutzt ausschließlich nur Konsolen-APIs zur Kommunikation und alle externen Algorithmen und Features liegen in den gleichen Quellen vor.

Der Grund für die Größenunterschiede sollte also in der “üblichen” Codegenerierung liegen …

GCC Binärausgaben verkleinern

Ich habe daher mal im Netz etwas herumgesucht, was man tun kann, um GCC Binaries zu verkleinern. Folgende Ansätze habe ich gefunden:

  • -Os Compiler Option anstatt von -O3 um für geringe Größe zu optimieren.
  • -flto Compiler und Linker Option um Link-Time-Optimization zu aktivieren.
  • -fdata-sections -ffunction-sections beim Kompilieren und -Wl,--gc-sections beim Linken einsetzen um unbenutzten Code und Daten zu entfernen.
  • -s Compiler und Linker Option zur Entfernung von Debugging-Symbolen aktivieren.

Nun kommt hinzu, dass ich aus Kompatibilitätsgründen nur CMake 3.0 nutze, welches die Link-Time-Optimization noch nicht selbst unterstützt, womit ich alle Compiler und Linker-Flags selbst setzen muss.

Ein Speziallfall ist auch -Os, denn CMake fügt automatisch ein -O3 hinten an, was mein -Os offenbar aufhebt. Daher muss dieses zuerst in CMake entfernt werden mit:
string(REPLACE "-O3" "" CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE}")

Nachdem diese Optimierungen nur im Falle eines GCC Compiler wirksam werden sollen, sieht mein CMakeLists.txt Update ungefähr so aus:

 1if(CMAKE_COMPILER_IS_GNUCC)
 2  string(REPLACE "-O3" "" CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE}")
 3  set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -flto -Os -s -fdata-sections -ffunction-sections")
 4  set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} -flto -s -Wl,--gc-sections")
 5endif(CMAKE_COMPILER_IS_GNUCC)
 6if(CMAKE_COMPILER_IS_GNUCXX)
 7  string(REPLACE "-O3" "" CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE}")
 8  set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -flto -Os -s -fdata-sections -ffunction-sections")
 9  set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} -flto -s -Wl,--gc-sections")
10endif(CMAKE_COMPILER_IS_GNUCXX)

Resultate

Nach einem vollständingen Rebuild nach einem make clean sieht die Tabelle jetzt anders aus:

Programm Plattform Neue Größe
gatecli.exe Windows x64 1 890 KB
gatecli Linux x64 1 621 KB
vtxtedit.exe Windows x64 287 KB
vtxtedit Linux x64 598 KB

Wow! gatecli ist unter Linux jetzt sogar kleiner als unter Windows. Fast könnte man glauben, dass der GCC jetzt bessere Resultate liefert als der MSVC (was im Detail auch stimmen kann), aber hier wirkt sich eher aus, dass einige Linux-Implementierungen auf meiner Seite nur NOT_IMPLEMENTED zurückliefern, während unter Windows schon ein paar hundert Zeilen abgetippt wurden.

Aber mit diesem Ergebnis kann ich mich schon ganz gut anfreunden.

Fazit

Link-Time-Optimierungen sind unter MSVC schon lange eine Default Einstellung, die der GCC jedoch explizit vergeschrieben braucht. Das Entfernen von Debug-Infos könnte man bei den Windows-Binaries natürlich auch aktivieren, aber meiner Erfahrung nach bringt das nur wenige Kilobytes.

Eher könnte man diverse Runtime-Sicherheitsfeatures deaktivieren, was ebenso in kleinerem generiertem Code-Output mündet.

Natürlich ist mir auch klar, dass mit dynamisch gelinkten C-Runtimes die Binärpakete um ein paar hundert Kilobytes kleiner werden können, doch dafür verlieren sie die Fähigkeit auf allen Systemen ohne Installtion der Runtime zu laufen … und dieses Opfer möchte ich nicht erbringen.

Wie auch immer. Mit ein paar Zeilen CMake wurde fast 1 MB unnötiger Ballast entfernt und die anderen hier nicht gelisteten Projekte profitieren ebenfalls von der Magerkur.

Tja, wieder was gelernt … alles in allem also ein erfolgreicher Tag.


Wenn sich eine triviale Erkenntnis mit Dummheit in der Interpretation paart, dann gibt es in der Regel Kollateralschäden in der Anwendung.
frei zitiert nach A. Van der Bellen
... also dann paaren wir mal eine komplexe Erkenntnis mit Klugheit in der Interpretation!

Meine Dokus über:
 
Externe Links zu: