Das ist sicherlich möglich, aber es ist eine unglaublich schwierige Aufgabe. Dies ist seit mehreren Jahrzehnten das zentrale Anliegen der Compiler-Forschung. Das Grundproblem besteht darin, dass wir kein Werkzeug entwickeln können, das die beste Aufteilung in Threads für Java-Code finden kann (dies entspricht dem Halteproblem).
Stattdessen müssen wir unser Ziel von der besten Partition in irgendeine Partition des Codes verlagern. Dies ist im Allgemeinen immer noch sehr schwierig. Wir müssen also Wege finden, um das Problem zu vereinfachen. Einer davon ist, den allgemeinen Code zu vergessen und sich mit spezifischen Programmtypen zu beschäftigen. Wenn man einen einfachen Kontrollfluss hat (konstante begrenzte for-Schleifen, begrenzte Verzweigungen....), kann man viel weiter kommen.
Eine weitere Vereinfachung ist die Verringerung der Anzahl der parallelen Einheiten, die Sie beschäftigen wollen. Nimmt man diese beiden Vereinfachungen zusammen, so erhält man den Stand der Technik bei der automatischen Vektorisierung (eine spezielle Art der Parallelisierung, die zur Erzeugung von Code im MMX/SSE-Stil verwendet wird). Es hat Jahrzehnte gedauert, diesen Stand zu erreichen, aber wenn man sich Compiler wie den von Intel ansieht, dann wird die Leistung langsam ziemlich gut.
Wenn man von Vektoranweisungen innerhalb eines einzelnen Threads zu mehreren Threads innerhalb eines Prozesses übergeht, erhöht sich die Latenzzeit beim Verschieben von Daten zwischen den verschiedenen Punkten im Code enorm. Das bedeutet, dass die Parallelisierung sehr viel besser sein muss, um den Kommunikations-Overhead auszugleichen. Dies ist derzeit ein sehr aktuelles Thema in der Forschung, aber es gibt keine automatischen, auf den Benutzer zugeschnittenen Tools. Wenn Sie eines schreiben können, das funktioniert, wäre das für viele Leute sehr interessant.
Wenn Sie für Ihr Beispiel davon ausgehen, dass rand() eine parallele Version ist, die unabhängig von verschiedenen Threads aufgerufen werden kann, dann ist es recht einfach zu erkennen, dass der Code in zwei Teile aufgeteilt werden kann. Ein Compiler müsste lediglich eine Abhängigkeitsanalyse durchführen, um festzustellen, dass keine der beiden Schleifen Daten der anderen verwendet oder diese beeinflusst. Die Reihenfolge zwischen den beiden Schleifen im Code auf Benutzerebene ist also eine falsche Abhängigkeit, die aufgespalten werden könnte (z. B. indem jede Schleife in einen separaten Thread gesetzt wird).
Aber das ist nicht wirklich die Art und Weise, wie Sie den Code parallelisieren wollen. Es sieht so aus, als ob jede Schleifeniteration von der vorherigen abhängt, da sum1 += rand(100) dasselbe ist wie sum1 = sum1 + rand(100), wobei sum1 auf der rechten Seite der Wert aus der vorherigen Iteration ist. Die einzige beteiligte Operation ist jedoch die Addition, die assoziativ ist, so dass wir die Summe auf viele verschiedene Arten umschreiben können.
sum1 = (((rand_0 + rand_1) + rand_2) + rand_3) ....
sum1 = (rand_0 + rand_1) + (rand_2 + rand_3) ...
Der Vorteil der zweiten Variante ist, dass jede einzelne Addition in Klammern parallel zu allen anderen berechnet werden kann. Sobald Sie 50 Ergebnisse haben, können diese zu weiteren 25 Additionen kombiniert werden und so weiter... Auf diese Weise wird mehr Arbeit geleistet: 50+25+13+7+4+2+1 = 102 Additionen im Vergleich zu 100 im Original, aber es gibt nur 7 sequenzielle Schritte, so dass es abgesehen vom parallelen Forking/Joining und dem Kommunikations-Overhead 14 Mal schneller geht. Dieser Baum von Additionen wird in parallelen Architekturen als Gathering-Operation bezeichnet und ist in der Regel der teuerste Teil einer Berechnung.
Auf einer sehr parallelen Architektur wie einer GPU wäre die obige Beschreibung der beste Weg, den Code zu parallelisieren. Bei der Verwendung von Threads innerhalb eines Prozesses würde dieser durch den Overhead zunichte gemacht werden.
Zusammengefasst Es ist unmöglich, es perfekt zu machen, es ist sehr schwer, es gut zu machen, und es gibt eine Menge aktiver Forschung, um herauszufinden, wie viel wir tun können.