Verwenden Compiler Multithreading für schnellere Kompilierungszeiten?

16

Wenn ich mich richtig an den Kurs meines Compilers erinnere, hat der typische Compiler die folgende vereinfachte Gliederung:

  • Einen lexikalischen Analysator Scans (oder einige Anrufe Abtastfunktion auf) der Quellcode Zeichen- für -Zeichen -
  • Die Zeichenfolge der eingegebenen Zeichen wird anhand des Lexemwörterbuchs auf Gültigkeit überprüft
  • Wenn das Lexem gültig ist, wird es als das Token klassifiziert, dem es entspricht
  • Der Parser überprüft die Syntax der Tokenkombination. Token für Token .

Ist es theoretisch machbar, den Quellcode in Viertel aufzuteilen (oder welchen Nenner auch immer) und den Scan- und Parsing-Prozess mit mehreren Threads auszuführen? Gibt es Compiler, die Multithreading verwenden?

8 Protonen
quelle
1
@RobertHarvey Die Antwort des ersten Links lautete: "Aber die Compiler selbst sind immer noch Singlethread-Compiler." Das ist also ein Nein?
8protons
Ich schlage vor, dass Sie den Rest der Antworten lesen, insbesondere diesen und den zweiten Link, den ich gepostet habe.
Robert Harvey
2
@RobertHarvey Der zweite Link, den Sie veröffentlicht haben, bezieht sich meines Wissens nach auf einen Compiler, der eine Multithread-Version Ihrer kompilierten Anwendung generiert. Es geht nicht um den Compiler selbst. Vielen Dank für Ihre freigegebenen Ressourcen und nehmen Sie sich Zeit, um zu antworten.
8protons

Antworten:

29

Große Softwareprojekte bestehen in der Regel aus vielen Kompilierungseinheiten, die relativ unabhängig kompiliert werden können. Daher wird die Kompilierung häufig mit einer sehr groben Granularität parallelisiert, indem der Compiler mehrmals parallel aufgerufen wird. Dies geschieht auf der Ebene der Betriebssystemprozesse und wird vom Build-System und nicht vom eigentlichen Compiler koordiniert. Mir ist klar, dass Sie dies nicht gefragt haben, aber das kommt der Parallelisierung in den meisten Compilern am nächsten.

Warum das? Ein Großteil der Arbeit, die Compiler leisten, lässt sich nicht ohne weiteres parallelisieren:

  • Sie können die Eingabe nicht einfach in mehrere Abschnitte aufteilen und diese unabhängig voneinander lexieren. Der Einfachheit halber möchten Sie Lexme-Grenzen aufteilen (sodass kein Thread in der Mitte eines Lexmes beginnt), aber das Ermitteln von Lexme-Grenzen erfordert möglicherweise viel Kontext. Wenn Sie beispielsweise in die Mitte der Datei springen, müssen Sie sicherstellen, dass Sie nicht in ein Zeichenfolgenliteral springen. Aber um dies zu überprüfen, müssen Sie sich im Grunde jeden Charakter ansehen, der vorher kam, was fast so viel Arbeit ist, als es einfach zu lexen. Außerdem ist Lexing selten der Engpass bei Compilern für moderne Sprachen.
  • Das Parsen ist noch schwieriger zu parallelisieren. Alle Probleme bei der Aufteilung des Eingabetexts für das Lexing gelten noch mehr für die Aufteilung der Token für das Parsen. Beispielsweise ist das Bestimmen, wo eine Funktion beginnt, im Grunde so schwierig wie das Parsen des Funktionsinhalts von Anfang an. Möglicherweise gibt es auch Möglichkeiten, diese zu umgehen, aber sie werden für den geringen Nutzen wahrscheinlich unverhältnismäßig komplex sein. Auch das Parsen ist nicht der größte Engpass.
  • Nach dem Parsen müssen Sie in der Regel eine Namensauflösung durchführen. Dies führt jedoch zu einem enormen Beziehungsgeflecht. Um einen Methodenaufruf hier aufzulösen, müssen Sie möglicherweise zuerst die Importe in diesem Modul auflösen. Für diese müssen jedoch die Namen in einer anderen Kompilierungseinheit usw. aufgelöst werden. Dasselbe gilt für Typinferenz, wenn Ihre Sprache über diese verfügt.

Danach wird es etwas einfacher. Die Typprüfung und -optimierung sowie die Codegenerierung können im Prinzip bei der Funktionsgranularität parallelisiert werden. Ich weiß immer noch von wenigen Compilern, die dies tun, vielleicht, weil es ziemlich schwierig ist, eine so große Aufgabe gleichzeitig zu erledigen. Sie müssen auch berücksichtigen, dass die meisten großen Softwareprojekte so viele Kompilierungseinheiten enthalten, dass der Ansatz "Mehrere Compiler gleichzeitig ausführen" völlig ausreicht, um alle Kerne (und in einigen Fällen sogar eine gesamte Serverfarm) zu belegen. Bei umfangreichen Kompilierungsaufgaben kann die Datenträger-E / A ebenso ein Engpass sein wie die eigentliche Kompilierungsarbeit.

Trotzdem kenne ich einen Compiler, der die Arbeit der Codegenerierung und -optimierung parallelisiert. Der Rust-Compiler kann die Back-End-Arbeit (LLVM, die eigentlich Codeoptimierungen enthält, die traditionell als "Middle-End" gelten) auf mehrere Threads aufteilen. Dies wird als "Code-Gen-Einheiten" bezeichnet. Im Gegensatz zu den anderen oben diskutierten Parallelisierungsmöglichkeiten ist dies wirtschaftlich, weil:

  1. Die Sprache verfügt über ziemlich große Kompilierungseinheiten (im Vergleich zu beispielsweise C oder Java), sodass möglicherweise weniger Kompilierungseinheiten im Flug sind als Kerne.
  2. Der Teil, der parallelisiert wird, benötigt normalerweise den größten Teil der Kompilierzeit.
  3. Die Backend-Arbeit verläuft zum größten Teil peinlich parallel - optimieren und übersetzen Sie einfach jede Funktion unabhängig in Maschinencode. Natürlich gibt es prozedurübergreifende Optimierungen, und Codegen-Einheiten behindern diese und wirken sich somit auf die Leistung aus, aber es gibt keine semantischen Probleme.

quelle
2

Kompilierung ist ein "peinlich paralleles" Problem.

Niemand kümmert sich um die Zeit zum Kompilieren einer Datei. Die Leute kümmern sich um die Zeit des Kompilierens von 1000 Dateien. Bei 1000 Dateien kann jeder Prozessorkern problemlos eine Datei gleichzeitig kompilieren, sodass alle Kerne voll ausgelastet sind.

Tipp: "make" verwendet mehrere Kerne, wenn Sie die richtige Befehlszeilenoption angeben. Andernfalls wird eine Datei nach der anderen auf einem 16-Kern-System kompiliert. Das heißt, Sie können die Kompilierung 16-mal schneller machen, indem Sie Ihre Build-Optionen in einer Zeile ändern.

gnasher729
quelle