Wie kompiliert Go so schnell?


216

Ich habe auf der Go-Website gegoogelt und gestöbert, aber ich kann anscheinend keine Erklärung für die außergewöhnlichen Bauzeiten von Go finden. Sind sie Produkte der Sprachfunktionen (oder deren Fehlen), ein hochoptimierter Compiler oder etwas anderes? Ich versuche nicht, Go zu fördern. Ich bin nur Neugierig.


12
@ Support, das ist mir bewusst. Ich denke, dass die Implementierung eines Compilers so, dass er mit spürbarer Schnelligkeit kompiliert werden kann, alles andere als eine vorzeitige Optimierung ist. Höchstwahrscheinlich ist es das Ergebnis guter Software-Design- und Entwicklungspraktiken. Ich kann es auch nicht ertragen, wenn Knuths Worte aus dem Zusammenhang gerissen und falsch angewendet werden.
Adam Crossland

55
Die pessimistische Version dieser Frage lautet: "Warum kompiliert C ++ so langsam?" stackoverflow.com/questions/588884/…
dan04

14
Ich habe dafür gestimmt, diese Frage erneut zu eröffnen, da sie nicht auf Meinungen basiert. Man kann einen guten technischen (nicht meinungsgebundenen) Überblick über die Auswahl der Sprache und / oder des Compilers geben, welche Geschwindigkeit die Kompilierung ermöglicht.
Martin Tournoij

Bei kleinen Projekten scheint mir Go langsam zu sein. Ich erinnere mich, dass Turbo-Pascal auf einem Computer, der wahrscheinlich tausendmal langsamer war, viel schneller war. prog21.dadgum.com/47.html?repost=true . Jedes Mal, wenn ich "go build" tippe und einige Sekunden lang nichts passiert, denke ich an knusprige alte Fortran-Compiler und Lochkarten zurück. YMMV. TLDR: "langsam" und "schnell" sind relative Begriffe.
RedGrittyBrick

Auf jeden Fall empfehlen, dave.cheney.net/2014/06/07/five-things-that-make-go-fast zu lesen, um detailliertere Einblicke zu erhalten
Karthik

Antworten:


192

Abhängigkeitsanalyse.

Die Go-FAQ enthielten den folgenden Satz:

Go bietet ein Modell für die Softwarekonstruktion, das die Abhängigkeitsanalyse vereinfacht und den Aufwand für Include-Dateien und -Bibliotheken im C-Stil weitgehend vermeidet.

Während der Satz nicht mehr in den FAQ enthalten ist, wird dieses Thema im Vortrag Go bei Google behandelt , in dem der Ansatz der Abhängigkeitsanalyse von C / C ++ und Go verglichen wird.

Das ist der Hauptgrund für die schnelle Kompilierung. Und das ist beabsichtigt.


Dieser Satz ist nicht mehr in den Go-FAQ enthalten, aber eine ausführlichere Erläuterung des Themas "Abhängigkeitsanalyse" zum Vergleich des C / C ++ - und Pascal / Modula / Go-Ansatzes finden Sie im Vortrag Go at Google
rob74

76

Ich denke, es ist nicht so, dass Go-Compiler schnell sind , sondern dass andere Compiler langsam sind .

C- und C ++ - Compiler müssen enorme Mengen an Headern analysieren - zum Kompilieren von C ++ "Hallo Welt" müssen beispielsweise 18.000 Codezeilen kompiliert werden, was fast einem halben Megabyte an Quellen entspricht!

$ cpp hello.cpp | wc
  18364   40513  433334

Java- und C # -Compiler werden in einer VM ausgeführt. Das bedeutet, dass das Betriebssystem die gesamte VM laden muss, bevor sie etwas kompilieren können. Anschließend müssen sie von Bytecode zu nativem Code JIT-kompiliert werden. Dies alles dauert einige Zeit.

Die Geschwindigkeit der Kompilierung hängt von mehreren Faktoren ab.

Einige Sprachen sind so konzipiert, dass sie schnell kompiliert werden können. Zum Beispiel wurde Pascal so konzipiert, dass es mit einem Single-Pass-Compiler kompiliert werden kann.

Auch die Compiler selbst können optimiert werden. Zum Beispiel wurde der Turbo Pascal-Compiler in einem handoptimierten Assembler geschrieben, was in Kombination mit dem Sprachdesign zu einem sehr schnellen Compiler führte, der auf Hardware der 286-Klasse arbeitete. Ich denke, dass moderne Pascal-Compiler (z. B. FreePascal) schon jetzt schneller sind als Go-Compiler.


19
Der C # -Compiler von Microsoft wird nicht in einer VM ausgeführt. Es ist immer noch in C ++ geschrieben, hauptsächlich aus Leistungsgründen.
Blucz

19
Turbo Pascal und später Delphi sind die besten Beispiele für blitzschnelle Compiler. Nachdem der Architekt von beiden auf Microsoft migriert ist, haben wir enorme Verbesserungen sowohl bei MS-Compilern als auch bei Sprachen festgestellt. Das ist kein Zufall.
TheBlastOne

7
18k Zeilen (18364 um genau zu sein) des Codes ist 433334 Bytes (~ 0,5MB)
el.pescado

9
Der C # -Compiler wird seit 2011 mit C # kompiliert. Nur ein Update für den Fall, dass jemand dies später liest.
Kurt Koller

3
Der C # -Compiler und die CLR, die die generierte MSIL ausführen, sind jedoch verschiedene Dinge. Ich bin ziemlich sicher, dass die CLR nicht in C # geschrieben ist.
Jocull

39

Es gibt mehrere Gründe, warum der Go-Compiler viel schneller ist als die meisten C / C ++ - Compiler:

  • Hauptgrund : Die meisten C / C ++ - Compiler weisen außergewöhnlich schlechte Designs auf (aus Sicht der Kompilierungsgeschwindigkeit). Aus Sicht der Kompilierungsgeschwindigkeit sind einige Teile des C / C ++ - Ökosystems (z. B. Editoren, in denen Programmierer ihre Codes schreiben) nicht auf Kompilierungsgeschwindigkeit ausgelegt.

  • Hauptgrund : Schnelle Kompilierungsgeschwindigkeit war eine bewusste Wahl im Go-Compiler und auch in der Go-Sprache

  • Der Go-Compiler hat einen einfacheren Optimierer als C / C ++ - Compiler

  • Im Gegensatz zu C ++ hat Go keine Vorlagen und keine Inline-Funktionen. Dies bedeutet, dass Go keine Vorlagen- oder Funktionsinstanziierung durchführen muss.

  • Der Go-Compiler generiert Assembler-Code auf niedriger Ebene früher und der Optimierer arbeitet mit dem Assembler-Code, während in einem typischen C / C ++ - Compiler die Optimierung die Arbeit an einer internen Darstellung des ursprünglichen Quellcodes übergibt. Der zusätzliche Aufwand im C / C ++ - Compiler ergibt sich aus der Tatsache, dass die interne Darstellung generiert werden muss.

  • Die endgültige Verknüpfung (5l / 6l / 8l) eines Go-Programms kann langsamer sein als die Verknüpfung eines C / C ++ - Programms, da der Go-Compiler den gesamten verwendeten Assemblycode durchläuft und möglicherweise auch andere zusätzliche Aktionen als C / C ++ ausführt Linker tun das nicht

  • Einige C / C ++ - Compiler (GCC) generieren Anweisungen in Textform (die an den Assembler übergeben werden sollen), während der Go-Compiler Anweisungen in Binärform generiert. Zusätzliche Arbeit (aber nicht viel) muss getan werden, um den Text in Binär umzuwandeln.

  • Der Go-Compiler zielt nur auf eine kleine Anzahl von CPU-Architekturen ab, während der GCC-Compiler auf eine große Anzahl von CPUs abzielt

  • Compiler wie Jikes, die mit dem Ziel einer hohen Kompilierungsgeschwindigkeit entwickelt wurden, sind schnell. Auf einer 2-GHz-CPU kann Jikes mehr als 20000 Zeilen Java-Code pro Sekunde kompilieren (und der inkrementelle Kompilierungsmodus ist noch effizienter).


17
Der Compiler von Go integriert kleine Funktionen. Ich bin mir nicht sicher, wie Sie durch das Targeting einer kleinen Anzahl von CPUs schneller langsamer werden ... Ich gehe davon aus, dass gcc beim Kompilieren für x86 keinen PPC-Code generiert.
Brad Fitzpatrick

@BradFitzpatrick hassen es, einen alten Kommentar wiederzubeleben, aber durch die Ausrichtung auf eine kleinere Anzahl von Plattformen können Entwickler des Compilers mehr Zeit damit verbringen, ihn für jeden einzelnen zu optimieren.
Ausdauer

Durch die Verwendung eines Zwischenformulars können Sie viel mehr Architekturen unterstützen, da Sie jetzt nur noch ein neues Backend für jede neue Architektur schreiben müssen
phuclv

34

Die Kompilierungseffizienz war ein wichtiges Entwurfsziel:

Schließlich soll es schnell gehen: Es sollte höchstens einige Sekunden dauern, bis eine große ausführbare Datei auf einem einzelnen Computer erstellt ist. Um diese Ziele zu erreichen, mussten eine Reihe von sprachlichen Problemen angegangen werden: ein ausdrucksstarkes, aber leichtes Typensystem; Parallelität und Speicherbereinigung; starre Abhängigkeitsspezifikation; und so weiter. FAQ

Die Sprach-FAQ ist ziemlich interessant in Bezug auf bestimmte Sprachfunktionen im Zusammenhang mit dem Parsen:

Zweitens wurde die Sprache so konzipiert, dass sie einfach zu analysieren ist und ohne Symboltabelle analysiert werden kann.


6
Das ist nicht wahr. Sie können den Go-Quellcode ohne Symboltabelle nicht vollständig analysieren.

12
Ich verstehe auch nicht, warum die Speicherbereinigung die Kompilierungszeiten verlängert. Das tut es einfach nicht.
TheBlastOne

3
Dies sind Zitate aus den FAQ: golang.org/doc/go_faq.html Ich kann nicht sagen, ob sie ihre Ziele nicht erreicht haben (Symboltabelle) oder ob ihre Logik fehlerhaft ist (GC).
Larry OBrien

5
@FUZxxl Gehen Sie zu golang.org/ref/spec#Primary_expressions und betrachten Sie die beiden Sequenzen [Operand, Call] und [Conversion]. Beispiel Go-Quellcode: Bezeichner1 (Bezeichner2). Ohne eine Symboltabelle kann nicht entschieden werden, ob es sich bei diesem Beispiel um einen Aufruf oder eine Konvertierung handelt. | Jede Sprache kann bis zu einem gewissen Grad ohne Symboltabelle analysiert werden. Es ist wahr, dass die meisten Teile von Go-Quellcodes ohne Symboltabelle analysiert werden können, aber es ist nicht wahr, dass es möglich ist, alle in der Golang-Spezifikation definierten Grammatikelemente zu erkennen.

3
@Atom Sie arbeiten hart daran, zu verhindern, dass der Parser jemals der Code ist, der einen Fehler meldet. Parser melden kohärente Fehlermeldungen im Allgemeinen schlecht. Hier erstellen Sie einen Analysebaum für den Ausdruck, als wäre er aTypeeine Variablenreferenz, und später in der Phase der semantischen Analyse, wenn Sie feststellen, dass zu diesem Zeitpunkt kein aussagekräftiger Fehler gedruckt wird.
Sam Harwell

26

Während die meisten der oben genannten Punkte zutreffen, gibt es einen sehr wichtigen Punkt, der nicht wirklich erwähnt wurde: das Abhängigkeitsmanagement.

Go muss nur die Pakete einschließen, die Sie direkt importieren (da diese bereits importiert haben, was sie benötigen). Dies steht in krassem Gegensatz zu C / C ++, wo jede einzelne Datei beginnt, einschließlich x-Headern, einschließlich y-Headern usw. Fazit: Die Kompilierung von Go dauert linear bis zur Anzahl der importierten Pakete, wobei C / C ++ exponentielle Zeit benötigt.


22

Ein guter Test für die Übersetzungseffizienz eines Compilers ist die Selbstkompilierung: Wie lange dauert es, bis ein bestimmter Compiler sich selbst kompiliert? Für C ++ dauert es sehr lange (Stunden?). Im Vergleich dazu würde sich ein Pascal / Modula-2 / Oberon-Compiler in weniger als einem kompilieren auf einer modernen Maschine Sekunde [1].

Go wurde von diesen Sprachen inspiriert, aber einige der Hauptgründe für diese Effizienz sind:

  1. Eine klar definierte, mathematisch fundierte Syntax für effizientes Scannen und Parsen.

  2. Eine typsichere und statisch kompilierte Sprache, die eine separate Kompilierung mit Abhängigkeits- und Typprüfung über Modulgrenzen hinweg verwendet, um unnötiges erneutes Lesen von Header-Dateien und erneutes Kompilieren anderer Module zu vermeiden - im Gegensatz zur unabhängigen Kompilierung wie in C / C ++, wo Der Compiler führt keine derartigen modulübergreifenden Überprüfungen durch (daher müssen alle Header-Dateien auch bei einem einfachen einzeiligen "Hallo Welt" -Programm immer wieder neu gelesen werden).

  3. Eine effiziente Compiler-Implementierung (z. B. Top-Down-Analyse mit rekursivem Abstieg in einem Durchgang) - was natürlich durch die obigen Punkte 1 und 2 erheblich unterstützt wird.

Diese Prinzipien sind bereits in den 1970er und 1980er Jahren in Sprachen wie Mesa, Ada, Modula-2 / Oberon und mehreren anderen bekannt und vollständig umgesetzt worden und finden erst jetzt (in den 2010er Jahren) Eingang in moderne Sprachen wie Go (Google). , Swift (Apple), C # (Microsoft) und einige andere.

Hoffen wir, dass dies bald die Norm und nicht die Ausnahme sein wird. Um dorthin zu gelangen, müssen zwei Dinge passieren:

  1. Zunächst sollten Anbieter von Softwareplattformen wie Google, Microsoft und Apple Anwendungsentwickler dazu ermutigen , die neue Kompilierungsmethode zu verwenden, und ihnen gleichzeitig ermöglichen, ihre vorhandene Codebasis wiederzuverwenden. Dies versucht Apple jetzt mit der Programmiersprache Swift zu tun, die mit Objective-C koexistieren kann (da dieselbe Laufzeitumgebung verwendet wird).

  2. Zweitens sollten die zugrunde liegenden Softwareplattformen selbst im Laufe der Zeit unter Verwendung dieser Prinzipien neu geschrieben werden, während gleichzeitig die Modulhierarchie neu gestaltet wird, um sie weniger monolithisch zu machen. Dies ist natürlich eine Mammutaufgabe und kann den größten Teil eines Jahrzehnts in Anspruch nehmen (wenn sie mutig genug sind, dies tatsächlich zu tun - was ich bei Google überhaupt nicht sicher bin).

In jedem Fall ist es die Plattform, die die Sprachakzeptanz vorantreibt, und nicht umgekehrt.

Verweise:

[1] http://www.inf.ethz.ch/personal/wirth/ProjectOberon/PO.System.pdf , Seite 6: "Der Compiler kompiliert sich in ca. 3 Sekunden selbst". Dieses Angebot gilt für eine kostengünstige Xilinx Spartan-3-FPGA-Entwicklungsplatine, die mit einer Taktfrequenz von 25 MHz und 1 MByte Hauptspeicher betrieben wird. Von hier aus kann man auf "weniger als 1 Sekunde" für einen modernen Prozessor extrapolieren, der mit einer Taktfrequenz weit über 1 GHz und mehreren GByte Hauptspeicher läuft (dh mehrere Größenordnungen leistungsstärker als die Xilinx Spartan-3 FPGA-Karte). auch unter Berücksichtigung der E / A-Geschwindigkeiten. Bereits 1990, als Oberon auf einem 25-MHz-NS32X32-Prozessor mit 2-4 MByte Hauptspeicher ausgeführt wurde, kompilierte sich der Compiler in wenigen Sekunden. Der Gedanke, tatsächlich zu warten dem Compiler leicht einen Kompilierungszyklus beenden, der Oberon-Programmierern schon damals völlig unbekannt war. Für typische Programme ist es immer so dauerte länger, den Finger von der Maustaste zu entfernen, die den Kompilierungsbefehl ausgelöst hat, als darauf zu warten, dass der Compiler die gerade ausgelöste Kompilierung abgeschlossen hat. Es war wirklich eine sofortige Befriedigung mit Wartezeiten nahe Null. Und die Qualität des produzierten Codes war für die meisten Aufgaben bemerkenswert gut und im Allgemeinen durchaus akzeptabel, obwohl sie nicht immer mit den besten damals verfügbaren Compilern vergleichbar war.


1
Ein Pascal / Modula-2 / Oberon / Oberon-2-Compiler würde sich in weniger als einer Sekunde auf einer modernen Maschine kompilieren [Zitat erforderlich]
CoffeeandCode

1
Zitat hinzugefügt, siehe Referenz [1].
Andreas

1
"... Prinzipien ... finden ihren Weg in moderne Sprachen wie Go (Google), Swift (Apple)" Ich bin mir nicht sicher, wie Swift in diese Liste aufgenommen wurde: Der Swift-Compiler ist eisig . Bei einem kürzlich abgehaltenen CocoaHeads Berlin-Treffen gab jemand einige Zahlen für ein mittelgroßes Framework an, sie erreichten 16 LOC pro Sekunde.
mpw

13

Go wurde entwickelt, um schnell zu sein, und es zeigt.

  1. Abhängigkeitsmanagement: Keine Header-Datei, Sie müssen sich nur die Pakete ansehen, die direkt importiert werden (Sie müssen sich keine Gedanken darüber machen, was sie importieren), sodass Sie lineare Abhängigkeiten haben.
  2. Grammatik: Die Grammatik der Sprache ist einfach und daher leicht zu analysieren. Obwohl die Anzahl der Features reduziert ist, ist der Compiler-Code selbst eng (wenige Pfade).
  3. Keine Überladung erlaubt: Sie sehen ein Symbol, Sie wissen, auf welche Methode es sich bezieht.
  4. Es ist trivial möglich, Go parallel zu kompilieren, da jedes Paket unabhängig kompiliert werden kann.

Beachten Sie, dass GO nicht die einzige Sprache mit solchen Funktionen ist (Module sind in modernen Sprachen die Norm), aber sie haben es gut gemacht.


Punkt (4) ist nicht ganz richtig. Von einander abhängige Module sollten in der Reihenfolge ihrer Abhängigkeit kompiliert werden, um modulübergreifendes Inlining und ähnliches zu ermöglichen.
Fuz

1
@FUZxxl: Dies betrifft jedoch nur die Optimierungsphase. Sie können eine perfekte Parallelität bis zur Backend-IR-Generierung erzielen. Es handelt sich also nur um eine modulübergreifende Optimierung, die in der Verbindungsphase durchgeführt werden kann, und die Verbindung ist ohnehin nicht parallel. Wenn Sie Ihre Arbeit nicht duplizieren möchten (erneutes Parsen), sollten Sie natürlich besser "gitter" kompilieren: 1 / Module ohne Abhängigkeit, 2 / Module nur abhängig von (1), 3 / Modulen abhängig nur von (1) und (2), ...
Matthieu M.

2
Dies ist mit einfachen Dienstprogrammen wie einem Makefile ganz einfach.
Fuz

12

Zitat aus dem Buch " The Go Programming Language " von Alan Donovan und Brian Kernighan:

Die Go-Kompilierung ist deutlich schneller als die meisten anderen kompilierten Sprachen, selbst wenn sie von Grund auf neu erstellt wird. Es gibt drei Hauptgründe für die Geschwindigkeit des Compilers. Zunächst müssen alle Importe explizit am Anfang jeder Quelldatei aufgelistet werden, damit der Compiler nicht eine gesamte Datei lesen und verarbeiten muss, um ihre Abhängigkeiten zu bestimmen. Zweitens bilden die Abhängigkeiten eines Pakets einen gerichteten azyklischen Graphen, und da es keine Zyklen gibt, können Pakete separat und möglicherweise parallel kompiliert werden. Schließlich zeichnet die Objektdatei für ein kompiliertes Go-Paket Exportinformationen nicht nur für das Paket selbst auf, sondern auch für seine Abhängigkeiten. Beim Kompilieren eines Pakets muss der Compiler für jeden Import eine Objektdatei lesen, darf jedoch nicht über diese Dateien hinausschauen.


9

Die Grundidee der Kompilierung ist eigentlich sehr einfach. Ein Parser mit rekursivem Abstieg kann im Prinzip mit E / A-gebundener Geschwindigkeit ausgeführt werden. Die Codegenerierung ist im Grunde ein sehr einfacher Prozess. Eine Symboltabelle und ein Basistypsystem erfordern nicht viel Berechnung.

Es ist jedoch nicht schwer, einen Compiler zu verlangsamen.

Wenn es eine Präprozessorphase mit mehrstufigen Include- Direktiven, Makrodefinitionen und bedingter Kompilierung gibt, so nützlich diese Dinge auch sind, ist es nicht schwer, sie herunterzuladen. (Zum Beispiel denke ich an die Windows- und MFC-Header-Dateien.) Deshalb sind vorkompilierte Header erforderlich.

In Bezug auf die Optimierung des generierten Codes gibt es keine Begrenzung, wie viel Verarbeitung zu dieser Phase hinzugefügt werden kann.


7

Einfach (in meinen eigenen Worten), weil die Syntax sehr einfach ist (zu analysieren und zu analysieren)

Zum Beispiel bedeutet keine Typvererbung, keine problematische Analyse, um herauszufinden, ob der neue Typ den vom Basistyp auferlegten Regeln folgt.

Beispiel: In diesem Codebeispiel: "Schnittstellen" überprüft der Compiler nicht, ob der beabsichtigte Typ die angegebene Schnittstelle implementiert , während er diesen Typ analysiert. Nur bis es verwendet wird (und WENN es verwendet wird), wird die Prüfung durchgeführt.

In einem anderen Beispiel teilt Ihnen der Compiler mit, ob Sie eine Variable deklarieren und nicht verwenden (oder ob Sie einen Rückgabewert halten sollen und nicht).

Folgendes wird nicht kompiliert:

package main
func main() {
    var a int 
    a = 0
}
notused.go:3: a declared and not used

Diese Art von Durchsetzungen und Prinzipien machen den resultierenden Code sicherer, und der Compiler muss keine zusätzlichen Überprüfungen durchführen, die der Programmierer durchführen kann.

Insgesamt erleichtern all diese Details das Parsen einer Sprache, was zu schnellen Kompilierungen führt.

Wieder in meinen eigenen Worten.


3

Ich denke, Go wurde parallel zur Compiler-Erstellung entwickelt, daher waren sie von Geburt an beste Freunde. (IMO)


0
  • Go importiert Abhängigkeiten einmal für alle Dateien, sodass die Importzeit nicht exponentiell mit der Projektgröße zunimmt.
  • Einfachere Linguistik bedeutet, dass die Interpretation weniger Rechenaufwand erfordert.

Was sonst?

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.