Über einen Blog
Artikel bin ich
auf die Idee gekommen mir die Build-Zeiten des Go-Compilers doch mal etwas
anzuschauen. Im Artikel wird festegstellt, dass bei einem Go-Build das Kommando
hg stat
im Hintergrund aufgerufen wird und dass dies vermutlich am längsten
dauert und die Build-Zeit verlängert:
Turns out go build will run hg stat for every build, which go run skips, and that was the slowest part of the process.
Ab Go Version 1.18 werden in der Tat genau diese Versionsinformationen, die
mit dem angegebenen Kommando ermittelt werden, in das resultierende Go-Binary
aufgenommen. Wie beschrieben passiert das nicht, wenn der Compiler über go run
aufgerufen wird (siehe z.
B.).
Selbstverständlich passiert das nicht nur bei Mercurial sondern auch bei den
anderen gängigen Versionsverwaltungswerkzeuge:
Release Notes Go 1.18
The go command now embeds version control information in binaries. It includes the currently checked-out revision, commit time, and a flag indicating whether edited or untracked files are present. Version control information is embedded if the go command is invoked in a directory within a Git, Mercurial, Fossil, or Bazaar repository, and the main package and its containing main module are in the same repository. This information may be omitted using the flag -buildvcs=false.
Möchte man die Information aus dem Binary auslesen kann dann anschließend das
go version
Kommandozeilenwerkzeug verwendet werden:
go version -m -v <binary>
Da das Feature allerdings erst in Go 1.18 eingeführt wurde, kam bei mir gleich die Frage auf:
Rein aus Interesse wollte ich herausfinden, wie sich denn die Build-Zeiten von Go über die unterschiedlichen Versionen verändert haben.
Der Plan sah so aus, dass ich ein Hello-World Beispiel einfach mit unterschiedlichen Go-Versionen kompiliere und dann die Zeiten miteinander vergleiche. Vielleicht ergibt sich ja was…
Die nächste Frage, die sich daraufhin gestellt hat war: Mit welchen Go-Versionen soll die Analyse stattfinden? Am Besten doch einfach mit Allen!
Mit Docker ist das natürlich auch recht einfach zu lösen. Über DockerHub werden alle bisher veröffentlichten Go-Versionen als Image zur Verfügung gestellt. Mit dieser Liste wollte ich also testen und das Hello-World Beispiel jeweils übersetzen und die Zeit messen.
Begonnen hatte ich mit der Abfrage der Images über wget und einer entsprechenden Auswertung mittels jq. Das hat aber dann schnell meine Schmerzgrenze beim Bash-Skripting erreicht und ich habe nach einer kurzen Suche das Hub-Tool von Docker selbst gefunden. Ein Kommandozeilenwerkzeug, das selbst in Go geschrieben ist, mit dem man recht einfach alle Docker-Image-Versionen abfragen kann.
Das Tool lässt sich super einfach installieren:
go install github.com/docker/hub-tool@latest
und dann nutzen:
hub-tool tag ls --all golang
Somit hatte ich also eine Liste der aller Go-Versionen, die ich dann doch
wieder mit Hilfe von jq
, cut
und grep
soweit zusammengedampft habe, dass
ich nur noch die “echten” Releases hatte. Also keine Beta oder
Sonstwas-Versionen.
...
1.14.7
1.14.8
1.14.9
1.15
1.15.0
1.15.1
1.15.10
1.15.11
1.15.12
1.15.13
1.15.14
...
Das waren dann immer noch 228 Versionen!
Nachdem ich also die Liste hatte konnte ich den ersten Versuch starten und über eine Schleife jeweils das Docker-Image herunterladen und die Anwendung darin kompilieren.
docker run --rm -v $(pwd):/go/src golang:$version /bin/bash -c 'time go build main.go';
Ausgeführt über alle Versionen brachte mich im ersten Schritt zu folgender Grafik:
WOW! ein Clickbait Bild! ;-)
Go Compiler ab Version 1.20 um Faktor 18 langsamer!
Ist natürlich Quatsch!, aber in der Grafik zugegebenermaßen beeindruckend. Der Grund ist recht einfach gefunden. Ab Version 1.20 werden keine pre-compiled Packages für die Standardbibliothek mehr ausgeliefert.
Release Notes Go 1.20
The directory $GOROOT/pkg no longer stores pre-compiled package archives for the standard library: go install no longer writes them, the go build no longer checks for them, and the Go distribution no longer ships them. Instead, packages in the standard library are built as needed and cached in the build cache, just like packages outside GOROOT. This change reduces the size of the Go distribution and also avoids C toolchain skew for packages that use cgo.
Das führt dazu, dass erstmals alle benötigten Teile der Standardbibliothek mit übersetzt werden müssen. Dieser ‘Overhead’ ist bei dem kleinen Hello World Beispiel recht massiv und wirkt sich sehr stark auf die Zeit aus.
Dieser ‘Overhead’ fällt natürlich nur einmalig an! Lokal in der Entwicklungsumgebung spielt das sicher keine Rolle. Vor Allem wenn es nicht wie auf meinem Test-Rechner 5s dauert. In CI/CD-Pipelines fällt dieser ‘Overhead’ aber potenziell immer an.
Um die Zeiten wieder ‘vergleichbarer’ zu machen habe ich einfach vor der Zeitmessung die Standardbibliothek übersetzt und somit eine ähnliche Grundlage für die Messungen geschaffen:
go install std
Ohne Übersetzen der Standardbibliothek ergab sich dann folgende Grafik:
Das relativiert die Ergebnisse doch schon gewaltig. Man sieht recht schön, dass die Build-Zeiten eigentlich seit Version 1.9 recht konstant geblieben sind. Ausreisser gib es immer wieder, was aber sicher auch an meinem ‘Test-Setup’ liegen kann.
Beispielzeiten:
1.16.7 93 ms
...
1.17 100 ms
...
1.18 137 ms
...
1.21.6 100 ms
Beachtlich ist der Unterschied von ca. 5 Sekunden bei einem Build auf meiner Maschine.
1.21.6 100 ms (precompiled) 5372 ms (clean)
Vielleicht könnte man in eingenen Images, die zum Bauen verwendet werden auch einfach die Standardbibliothek vorab übersetzen, dann muss das nicht in jedem Projekt neu gemacht werden!
Beispielrechnung: 5s * 10 (mal am Tag gebaut) * 10 (Projekte) * 248 (Arbeitstage) = 124000 s = ~34 Stunden - WOW! - Ob jetzt realistisch oder nicht -> kleiner Aufwand -> Wirkung
Ok, das eigentliche war doch das buildvcs
Flag. Wie ändern sich nun hier die
Build-Zeiten? Da das Flag erst in Version 1.18 eingeführt wurde, verkürzt sich
dementsprechend auch die Messreihe.
Verglichen habe ich die Build-Zeit mit einem Precompiled-Build mit
Standardeinstellung buildvcs
und mit buildvcs=false
. Im ersten Schritt mit
vorhandenem .git
Verzeichnis.
Die Grafik zeigt erstmal den ‘Overhead’ (Splate G sind Millisekunden):
Also eigentlich “kein Unterschied”. Ich würde auf ‘Messungenauigkeit’ tippen. Alles hier mit Git getestet, eventuell ist es bei Mercurial tatsächlich etwas Anderes.
Go-Version Prebuild time Prebuild ohne buildvcs
...
1.19.2 167 ms 109 ms
...
1.19.3 129 ms 129 ms
...
1.21.6 100 ms 98 ms
Dazu habe ich noch eine zweite Messreihe erstellt ohne .git
Verzeichnis, da
schwankten die Zeiten auch soweit, dass man nicht wirklich was sagen kann. Es
scheint aber kein großen Unterschied zu machen.
BuildVCS scheint in meinem Setup mit git keinen großen Unterschied zu machen.
Auf einen Vergleich von Mercurial und Git habe ich verzichtet. Ob das einen Mehrwert liefern würde weiß ich nicht…
Interessiert hat mich am Ende noch die Anwendungsgröße. Wie hat sich die Dateigröße über die Go-Versionen hinweg verändert?
Auch hier hat sich wenig getan. Die Dateigößen schwanken etwas. In Version 1.8 waren es mal 1,5 MB. Ansonsten gewegte sich die Größe so um die 2 MB. Aktuell sind es bei mir ca. 1,8 MB.
Schlußendlich muss man sagen, das mich das Ergebnis beim pre-compile schon etwas überrascht hat. Auch ein Projektsetup zu finden, dass in allen Go-Versionen 1:1 funktioniert, war auch etwas überraschend. Die Einführung des Module-Supports oder die unterschiedlichen Compiler-Flags, haben das ganze doch etwas komplizierter gemacht, wie ursprungs gedacht.
Klar ist auch, dass die Welt sich bei einem “Hello World”-Beispiel auch anders dreht wie bei einer größeren Anwendung. Was man sich sicher merken kann:
go run
compiliert potentiell etwas flotter, das Binary enthält aber keine
Versionsinformation.Compatibility is at the source level. Binary compatibility for compiled packages is not guaranteed between releases. After a point release, Go source will need to be recompiled to link against the new release.
Der nächste Schritt sollte jetzt noch ein Vergleich einer “größeren Anwendung” sein. Gibt es Unterschiede wenn man z. B. einen Microservice compiliert? Fallen die Unterschiede wirklich ins Gewicht… wahrscheinlich nicht, aber wer weiß…
24.01.2024
Der Author auf LinkedIn: Kristian Köhler und Mastodon: @kkoehler@mastodontech.de
Der praktische Soforteinstieg für Developer und Softwarearchitekten, die direkt mit Go produktiv werden wollen.
zur Buchseite beim Rheinwerk Verlag Rheinwerk Computing, ISBN 978-3-8362-7559-0 (als PDF, EPUB, MOBI und Papier)
Source Fellows GmbH
Lerchenstraße 31
72762 Reutlingen
Telefon: (0049) 07121 6969 802
E-Mail: info@source-fellows.com