it-swarm.com.de

Verwenden Compiler Multithreading für schnellere Kompilierungszeiten?

Wenn ich mich richtig an meinen Compilerkurs erinnere, hat der typische Compiler die folgende vereinfachte Gliederung:

  • Ein lexikalischer Analysator scannt (oder ruft eine Scanfunktion auf) den Quellcode zeichenweise
  • Die Zeichenfolge der eingegebenen Zeichen wird anhand des Lexikonwö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 Kombination von Token. Token für Token.

Ist es theoretisch möglich, den Quellcode in Viertel (oder einen beliebigen Nenner) aufzuteilen und den Scan- und Analyseprozess multithread zu machen? Gibt es Compiler, die Multithreading verwenden?

16
8protons

Große Softwareprojekte bestehen normalerweise 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 ist das so? Nun, ein Großteil der Arbeit, die Compiler leisten, eignet sich nicht leicht für die Parallelisierung:

  • Sie können die Eingabe nicht einfach in mehrere Blöcke aufteilen und diese unabhängig voneinander Lex. Der Einfachheit halber möchten Sie die Lexme-Grenzen aufteilen (damit kein Thread in der Mitte eines Lexme beginnt), aber das Bestimmen der 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, muss man sich im Grunde jeden Charakter ansehen, der vorher kam, was fast so viel Arbeit ist, als es zunächst nur zu lexen. Außerdem ist Lexing selten der Engpass bei Compilern für moderne Sprachen.
  • Das Parsen ist noch schwieriger zu parallelisieren. Alle Probleme beim Aufteilen des Eingabetextes zum Lexen betreffen noch mehr das Aufteilen der Token zum Parsen - z. B. ist das Bestimmen, wo eine Funktion beginnt, im Grunde so schwierig wie das Parsen des Funktionsinhalts. Es gibt zwar auch Möglichkeiten, dies zu umgehen, aber sie werden wahrscheinlich für den geringen Nutzen unverhältnismäßig komplex sein. Auch das Parsen ist nicht der größte Engpass.
  • Nach dem Parsen müssen Sie normalerweise eine Namensauflösung durchführen. Dies führt jedoch zu einem riesigen Netz von Beziehungen. 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. Gleiches gilt für die Typinferenz, wenn Ihre Sprache dies hat.

Danach wird es etwas einfacher. Die Typprüfung und -optimierung sowie die Codegenerierung können im Prinzip bei der Funktionsgranularität parallelisiert werden. Ich kenne immer noch wenige Compiler, 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 parallel ausführen" völlig ausreicht, um alle Ihre Kerne (und in einigen Fällen sogar eine gesamte Serverfarm) belegt zu halten. Außerdem kann bei großen Kompilierungsaufgaben die Festplatten-E/A ebenso ein Engpass sein wie die eigentliche Kompilierungsarbeit.

Trotzdem kenne ich einen Compiler, der die Arbeit der Codegenerierung und -optimierung parallelisiert. Der Compiler Rust) kann die Back-End-Arbeit (LLVM, die tatsächlich Codeoptimierungen enthält, die traditionell als "mittleres Ende" betrachtet werden) 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 Kompilierungszeit.
  3. Die Backend-Arbeit ist größtenteils peinlich parallel - optimieren Sie einfach jede Funktion und übersetzen Sie sie in Maschinencode. Natürlich gibt es prozedurale Optimierungen, und Codegen-Einheiten behindern diese und wirken sich somit auf die Leistung aus, aber es gibt keine semantischen Probleme.
29
user7043

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. Und für 1000 Dateien kann jeder Kern des Prozessors problemlos jeweils eine Datei kompilieren, sodass alle Kerne voll ausgelastet sind.

Tipp: "make" verwendet mehrere Kerne, wenn Sie ihm die richtige Befehlszeilenoption geben. Ohne dies wird eine Datei nach der anderen auf einem 16-Kern-System kompiliert. Das heißt, Sie können es 16-mal schneller kompilieren lassen, indem Sie Ihre Build-Optionen um eine Zeile ändern.

2
gnasher729