Erkennen Compiler wie Javac reine Funktionen automatisch und parallelisieren sie?

12

Es ist bekannt, dass reine Funktionen das Parallelisieren erleichtern. Was ist an der funktionalen Programmierung, die sie an die parallele Ausführung anpasst?

Sind Compiler wie Javac intelligent genug, um zu erkennen, ob eine Methode eine reine Funktion ist? Man kann immer Klassen implementieren, die funktionale Schnittstellen wie Function implementieren , aber Nebenwirkungen haben.

Naveen
quelle
7
Die Frage ist nicht nur, ob der Compiler wissen kann, ob eine Funktion rein ist, sondern auch, ob er die parallele Ausführung reiner Funktionen intelligent planen kann. Es reicht nicht aus, für jeden einen neuen Thread auszulösen: Das ist ineffizient. GHC (Haskell) befasst sich mit Faulheit und "grünen Fäden"; Ich wäre ehrlich gesagt überrascht, wenn eine unreine Sprache es überhaupt versuchen würde, da es zusätzlich schwierig ist, sicherzustellen, dass die reinen Threads in Bezug auf den unreinen Hauptthread korrekt geplant wurden.
Ryan Reich
@ RyanReich, gibt es Leistungsgewinne durch die Verwendung von funktionaler Programmierung in einer unreinen funktionalen Sprache wie Java? Sind die Vorteile der funktionalen Programmierung rein funktional, wie z. B. die Modularität?
Naveen
@RyanReich GHC löst das Problem, indem der Programmierer Anmerkungen hinzufügt, wenn Parallelität gewünscht wird. Reinheit impliziert, dass diese Annotationen niemals die Semantik ändern, sondern nur die Leistung. (Es gibt auch Parallelitätsmechanismen, die zu Parallelität führen können, aber dies ist ein anderer Fischkessel.)
Derek Elkins verließ SE
@Naveen Reine Funktionen bieten neben der Parallelität weitere Vorteile in Bezug auf die Optimierung, z. B. eine größere Freiheit beim Neuordnen von Code, das Speichern von Memos und die Eliminierung häufiger Teilausdrücke. Ich könnte mich irren, aber ich bezweifle, dass Javac versucht, Reinheit zu erkennen, da dies im idiomatischen Code wahrscheinlich ziemlich selten und für alle, außer für die trivialsten Fälle, etwas schwierig ist. Zum Beispiel müssen Sie wissen, dass es keine NullPointerExceptions gibt. Die Vorteile von Optimierungen auf dieser Basis sind für typische Java-Anwendungen wahrscheinlich ebenfalls recht gering.
Derek Elkins verließ SE
6
javac ist der Java-Compiler, der Java-Quellcode verwendet und Java-Bytecode-Klassendateien generiert. Es ist ziemlich eingeschränkt, was es kann (und soll). Es verfügt weder über die Freiheit noch über die notwendigen zugrunde liegenden Mechanismen, um Parallelität in die Bytecode-Klassendatei einzuführen.
Erik Eidt

Antworten:

33

Sind Compiler wie Javac intelligent genug, um zu erkennen, ob eine Methode eine reine Funktion ist?

Es ist keine Frage von "klug genug". Dies wird als Reinheitsanalyse bezeichnet und ist im allgemeinen Fall nachweislich unmöglich: Es ist gleichbedeutend mit der Lösung des Halteproblems.

Jetzt tun Optimierer natürlich die ganze Zeit nachweislich Unmögliches. "Nachweislich Unmöglich im Allgemeinen" bedeutet nicht, dass es niemals funktioniert, sondern nur, dass es nicht in allen Fällen funktioniert. Es gibt also tatsächlich Algorithmen, mit denen überprüft werden kann, ob eine Funktion rein ist oder nicht. Oftmals lautet das Ergebnis "Ich weiß nicht", was bedeutet, dass Sie aus Gründen der Sicherheit und Richtigkeit davon ausgehen müssen dass diese bestimmte Funktion unrein sein könnte.

Und selbst in den Fällen, in denen es funktioniert , sind die Algorithmen komplex und teuer.

Das ist also Problem Nr. 1: Es funktioniert nur für Sonderfälle .

Problem Nr. 2: Bibliotheken . Damit eine Funktion rein ist, kann sie immer nur reine Funktionen aufrufen (und diese Funktionen können nur reine Funktionen aufrufen usw.). Javac kennt sich offensichtlich nur mit Java aus, und es kennt sich nur mit Code aus, den es sehen kann. Wenn Ihre Funktion also eine Funktion in einer anderen Kompilierungseinheit aufruft, können Sie nicht wissen, ob sie rein ist oder nicht. Wenn es eine Funktion aufruft, die in einer anderen Sprache geschrieben ist, können Sie es nicht wissen. Wenn es eine Funktion in einer Bibliothek aufruft, die möglicherweise noch nicht installiert ist, wissen Sie es nicht. Und so weiter.

Dies funktioniert nur, wenn Sie eine Ganzprogrammanalyse haben, wenn das gesamte Programm in derselben Sprache geschrieben ist und alles auf einmal kompiliert wird. Sie können keine Bibliotheken verwenden.

Problem Nr. 3: Planung . Sobald Sie herausgefunden haben, welche Teile rein sind, müssen Sie sie noch für separate Threads einplanen. Oder nicht. Das Starten und Stoppen von Threads ist sehr teuer (insbesondere in Java). Auch wenn Sie einen Thread-Pool führen und diese nicht starten oder stoppen, ist das Wechseln des Thread-Kontextes ebenfalls teuer. Sie müssen sicherstellen, dass die Berechnung erheblich länger ausgeführt wird als für die Planung und den Kontextwechsel erforderlich. Andernfalls verlieren Sie die Leistung und gewinnen sie nicht.

Wie Sie wahrscheinlich schon vermutet haben, ist es im allgemeinen Fall nachweislich unmöglich, herauszufinden, wie lange eine Berechnung dauern wird (wir können nicht einmal herausfinden, ob sie eine begrenzte Zeit in Anspruch nehmen wird, geschweige denn, wie viel Zeit) und selbst in diesem Fall schwierig und teuer der Sonderfall.

Nebenbei: Javac und Optimierungen . Beachten Sie, dass die meisten Implementierungen von Javac nicht viele Optimierungen durchführen. Die Java-Implementierung von Oracle basiert beispielsweise auf der zugrunde liegenden Ausführungs-Engine, um Optimierungen vorzunehmen . Dies führt zu einer Reihe weiterer Probleme: Zum Beispiel entschied Javac, dass eine bestimmte Funktion rein und teuer genug ist, und kompiliert sie, um auf einem anderen Thread ausgeführt zu werden. Dann kommt der Optimierer der Plattform (zum Beispiel der HotSpot C2 JIT-Compiler) und optimiert die gesamte Funktion weg. Jetzt haben Sie einen leeren Thread, der nichts tut. Oder stellen Sie sich vor, Javac entschließt sich erneut, eine Funktion für einen anderen Thread zu planen, und der Plattformoptimierer könnte dies tun Optimieren Sie es vollständig, es sei denn, es kann kein Inlining über Threadgrenzen hinweg ausgeführt werden. Daher wird eine Funktion, die vollständig optimiert werden kann, jetzt unnötig ausgeführt.

So etwas zu tun ist nur dann wirklich sinnvoll, wenn Sie einen einzelnen Compiler haben, der die meisten Optimierungen auf einmal ausführt, sodass der Compiler alle verschiedenen Optimierungen auf verschiedenen Ebenen und deren Interaktionen miteinander kennt und ausnutzen kann.

Beachten Sie, dass zum Beispiel die HotSpot C2 JIT - Compiler tatsächlich tut einige Auto-Vektorisierung durchführen, die auch eine Form der Auto-Parallelisierung.

Jörg W. Mittag
quelle
Abhängig von Ihrer Definition der "reinen Funktion" kann die Verwendung unreiner Funktionen in der Implementierung zulässig sein.
Deduplizierer
@ Deduplicator Nun, je nach Ihrer Definition von definition, ist die Verwendung eines disparaten definitionvon puritywahrscheinlich dunkel
Katze
1
Ihr Problem Nr. 2 wird größtenteils dadurch ungültig, dass praktisch alle Optimierungen vom JIT ausgeführt werden (Sie wissen es offensichtlich, ignorieren es jedoch). In ähnlicher Weise wird das Problem Nr. 3 teilweise ungültig, da die JIT stark von den vom Interpreter gesammelten Statistiken abhängt. Ich bin insbesondere anderer Meinung als "Sie können keine Bibliotheken verwenden", da es zur Rettung eine Deoptimierung gibt. Ich stimme zu, dass die zusätzliche Komplexität ein Problem darstellen würde.
Maaartinus
2
@ maaartinus: Außerdem ist nur das Ende meiner Antwort spezifisch für javac. Ich habe ausdrücklich tun Erwähnung, zum Beispiel, dass „Dies funktioniert nur, wenn Sie ganze Programmanalyse, wenn das gesamte Programm in derselben Sprache geschrieben ist, und alle auf einmal auf einmal zusammengestellt.“ Dies gilt natürlich auch für C2: Es handelt sich nur um eine Sprache (JVM-Bytecode), und es hat gleichzeitig Zugriff auf das gesamte Programm.
Jörg W Mittag
1
@ JörgWMittag Ich weiß, dass das OP nach Javac fragt, aber ich wette, sie gehen davon aus, dass Javac für Optimierungen verantwortlich ist. Und dass sie kaum wissen, dass es C2 gibt. Ich sage nicht, deine Antwort ist schlecht. Es ist nur so, dass es unsinnig ist, javac eine Optimierung vornehmen zu lassen (mit Ausnahme von Trivialitäten wie der Verwendung StringBuilder). Ich würde es also ablehnen und einfach annehmen, dass das OP javac schreibt, aber Hotspot bedeutet. Ihr Problem Nr. 2 ist ein guter Grund, nichts in Java zu optimieren .
Maaartinus
5

Die überstimmte Antwort bemerkte nichts. Die synchrone Kommunikation zwischen Threads ist extrem teuer. Wenn die Funktion mit einer Rate von vielen Millionen Aufrufen pro Sekunde ausgeführt werden kann, tut es Ihnen mehr weh, sie zu parallelisieren, als sie unverändert zu lassen.

Die schnellste Form der synchronen Inter-Thread-Kommunikation unter Verwendung von Busy-Loops mit atomaren Variablen ist leider ineffizient. Wenn Sie auf Bedingungsvariablen zurückgreifen müssen, um Energie zu sparen, leidet die Leistung Ihrer Inter-Thread-Kommunikation.

Der Compiler muss also nicht nur feststellen, ob eine Funktion rein ist, sondern auch die Ausführungszeit der Funktion abschätzen, um festzustellen, ob die Parallelisierung ein Nettogewinn ist. Außerdem müsste zwischen Busy-Loops mit atomaren Variablen oder Bedingungsvariablen gewählt werden. Und es müsste Fäden hinter Ihrem Rücken erstellen.

Wenn Sie die Threads dynamisch erstellen, ist dies sogar langsamer als die Verwendung von Bedingungsvariablen. Der Compiler müsste also eine bestimmte Anzahl von Threads einrichten, die bereits ausgeführt werden.

Die Antwort auf Ihre Frage lautet also Nein . Compiler sind nicht "schlau" genug, um reine Funktionen automatisch zu parallelisieren, insbesondere in der Java-Welt. Sie sind schlau, wenn sie nicht automatisch parallelisiert werden!

juhist
quelle
5
" Sie sind schlau, wenn sie nicht automatisch parallelisiert werden! " : Dies geht zu weit. Zwar wäre eine Parallelisierung an jedem möglichen Punkt, nur um ihrer selbst willen, im Allgemeinen ineffizient, doch würde ein intelligenter Compiler eine praktische Parallelisierungsstrategie identifizieren. Ich denke, dass die meisten Leute das verstehen. Wenn wir also von Auto-Parallelisierung sprechen, meinen wir Auto-Praktische-Parallelisierung.
Nat
@ Nat: Lächerlich zu hart. Dies würde die Identifizierung reiner Funktionen auf der Laufzeitskala von Hunderten von Millisekunden erfordern, und es wäre dumm, wenn der Compiler eine Vorstellung von der Laufzeit von Schleifen bekommen würde, deren Iterationen keine Konstanten enthalten (und die Fälle, die Sie nicht möchten).
Joshua
Ich stimme zu - @Nats Kommentar impliziert, dass Parallelisierung nicht unbedingt mehrere Threads bedeutet, was wahr ist. Die JIT könnte beispielsweise mehrere Aufrufe einer reinen Funktion einbinden und in bestimmten Fällen ihre CPU-Anweisungen verschachteln. Wenn beispielsweise beide Methodenaufrufe eine Konstante abrufen, kann diese einmal abgerufen und in einem CPU-Register für beide zu verwendenden Instanzen der Methode gespeichert werden. Moderne CPUs sind Biester mit zahlreichen Allzweckregistern und speziellen Anweisungen, die bei der Optimierung von Code hilfreich sein können.
1
@Joshua: Viel einfacher für einen JIT-Compiler. Der JIT-Compiler kann auch herausfinden, dass eine Funktion möglicherweise nicht rein ist, aber kein Aufruf hat bisher unreines Verhalten hervorgerufen.
gnasher729
Ich bin mit @Joshua einverstanden. Ich arbeite mit einem schwer zu parallelisierenden Algorithmus. Ich habe versucht, es manuell zu parallelisieren, auch indem ich einige vereinfachende Näherungen (und damit den Algorithmus) vorgenommen habe, und bin jedes Mal kläglich gescheitert. Sogar ein Programm, das sagt, ob es machbar ist, etwas zu parallelisieren, ist äußerst schwierig, auch wenn es viel einfacher wäre, als es tatsächlich zu parallelisieren. Denken Sie daran, wir sprechen von Turing-vollständigen Programmiersprachen.
Juhist