Parallele Ausführung
PetaPerl parallelisiert geeignete Schleifen automatisch mit Rayon, einem Work-Stealing-Thread-Pool. In Kombination mit JIT-Kompilierung ermöglicht dies dramatische Beschleunigungen für rechenintensive Aufgaben.
Funktionsweise
Wenn PetaPerl eine parallelisierbare Schleife erkennt, geschieht Folgendes:
- Analyse des Schleifenrumpfes auf Seiteneffekte und gemeinsam veränderlichen Zustand
- Identifikation von Reduktionsvariablen (Akkumulatoren wie
$sum += ...) - Verteilung der Iterationen über Threads mittels Rayons Work-Stealing-Scheduler
- Zusammenführung der Ergebnisse unter Verwendung der erkannten Reduktionsoperationen
Jeder Thread erhält seine eigene Kopie der schleifen-lokalen Variablen. Reduktionsvariablen werden nach Abschluss aller Threads zusammengeführt.
Was parallelisiert wird
JIT-kompilierte While-Schleifen
Wenn der JIT eine While-Schleife kompiliert und die Analyse Folgendes erkennt:
- Eine Zählvariable mit bekannten Grenzen
- Reduktionsvariablen (reines Akkumulationsmuster)
- Keine I/O-Operationen oder Seiteneffekte im Schleifenrumpf
Dann wird der Schleifenrumpf einmal kompiliert und parallel über mehrere Threads ausgeführt.
Eingebaute Funktionen
map und grep mit reinen Callbacks können parallel ausgeführt werden:
my @results = map { expensive_computation($_) } @large_array;
my @filtered = grep { complex_test($_) } @large_array;
Voraussetzungen für Parallelisierung:
- Keine Seiteneffekte im Callback
- Kein gemeinsam veränderlicher Zustand
- Sammlungsgröße über dem Parallelisierungs-Schwellenwert
CLI-Steuerung
# Standard: Parallelisierung aktiviert
pperl script.pl
# Parallelisierung deaktivieren
pperl --no-parallel script.pl
# Explizit aktivieren (Standard)
pperl --parallel script.pl
# Thread-Anzahl festlegen (Standard: Anzahl der CPU-Kerne)
pperl --threads=4 script.pl
# Minimale Sammlungsgröße für Parallelisierung festlegen
pperl --parallel-threshold=1000 script.pl
Der Testrahmen läuft standardmäßig mit --no-parallel, um deterministische Testausgaben sicherzustellen.
Leistung
Mandelbrot-Menge (1000x1000)
| Modus | Zeit | vs. perl5 |
|---|---|---|
| perl5 | 12.514ms | 1,0x |
| pperl Interpreter | ~3.500ms | 3,6x schneller |
| pperl JIT | 163ms | 76x schneller |
| pperl JIT + parallel (8 Threads) | 29ms | 431x schneller |
Skalierung
Der Work-Stealing-Scheduler liefert nahezu lineare Skalierung für trivial parallelisierbare Aufgaben:
| Threads | Mandelbrot 4000x4000 | Skalierung |
|---|---|---|
| 1 | Basislinie | 1,0x |
| 2 | ~50% Zeit | ~1,9x |
| 4 | ~25% Zeit | ~3,8x |
| 8 | ~13% Zeit | ~5,2x |
Die Skalierung ist sublinear aufgrund von Speicherbandbreite, Cache-Effekten und Reduktions-Overhead.
Einschränkungen
String-Operationen
String-Operationen (.= Concat, String-Aufbau) werden nicht parallelisiert. Die JIT-String-Unterstützung verwendet Extern Calls zurück in die Rust-Laufzeitumgebung, die veränderlichen Zugriff auf gemeinsamen Zustand erfordert. Wenn der JIT String-Variablen in einer Schleife erkennt, wird die parallele Ausführung deaktiviert.
Seiteneffekt-Erkennung
Der Parallelisierungs-Analysator ist konservativ. Jeder der folgenden Punkte schließt eine Schleife aus:
- I/O-Operationen (
print,open, Datei-Lesezugriffe) - Schreiben globaler Variablen
- Unterprogrammaufrufe (sofern nicht als rein nachgewiesen)
- Regex-Operationen mit Seiteneffekten (
s///)
Falsch-Negative (verpasste Parallelisierungsmöglichkeiten) sind sicher — die Schleife wird einfach sequenziell ausgeführt. Falsch-Positive (fehlerhafte Parallelisierung) wären Fehler.
Determinismus
Parallele Ausführung kann die Reihenfolge von Seiteneffekten verändern. Aus diesem Grund wird Parallelisierung nur angewendet, wenn die Analyse beweist, dass der Schleifenrumpf frei von beobachtbaren Seiteneffekten ist.
Die Ausgabereihenfolge wird bei map und grep beibehalten — das Ergebnis-Array behält dieselbe Elementreihenfolge wie bei sequenzieller Ausführung.
Wie die Reduktionserkennung funktioniert
Der Analysator identifiziert Reduktionsvariablen, indem er nach Akkumulationsmustern sucht und Rücksetzmuster davon abzieht:
# Als Reduktion erkannt: $sum akkumuliert, wird nie in der Schleife zurückgesetzt
my $sum = 0;
for my $x (@data) {
$sum += $x;
}
# KEINE Reduktion: $temp wird bei jeder Iteration zurückgesetzt
for my $x (@data) {
my $temp = $x * 2; # Rücksetzung (my-Deklaration)
$sum += $temp; # $sum ist weiterhin eine Reduktion
}
Die Formel: Reduktionen = Akkumulationen - Rücksetzungen. Dies verhindert Falsch-Positive, bei denen eine Variable innerhalb des Schleifenrumpfes sowohl akkumuliert als auch zurückgesetzt wird.