Blog

Code-Performanz in R: Parallelisierung

30.06.2021 16:00
von

Dies ist der dritte Teil unserer Serie über Code-Performanz in R. Im ersten Teil ging es darum, den langsamen Teil den Codes zu identifizieren und Code-Geschwindigkeit zu messen. Der zweite Teil umfasste allgemeine Techniken zur Beschleunigung von R-Code. Dieser Teil widmet sich nun komplett der Parallelisierung in R.

Was ist Parallelisierung?

Oft werden im Code mehrere, völlig unabhängige Schritte ausgeführt, zum Beispiel bei einer Simulation mit fünf verschiedenen Parameter-Sets. Diese fünf Prozesse müssen dann untereinander nicht kommunizieren und brauchen keine Ergebnisse aus den anderen Prozessen. Sie könnten damit theoretisch sogar auf fünf verschiedenen Computern laufen... oder auf fünf verschiedenen Prozessorkernen: Das nennt man Parallelisierung.

Moderne Computer haben in der Regel mindestens 16 Prozessorkerne. Wieviele Kerne ein PC hat, lässt sich z.B. mit der R-Funktion detectCores() leicht herausfinden. Standardmäßig nutzt R immer nur einen Kern, aber in diesem Artikel werden wir sehen, wie man mehrere gleichzeitig verwenden kann. So kann eine 20-stündige Simulation am Ende innerhalb weniger Stunden durchlaufen!

Wann lohnt sich Parallelisierung?

Je mehr Parallelisierung, desto besser? So einfach ist es leider nicht. Es kostet schon eine gewisse Zeit, die Parallelisierung zu initialisieren, beispielsweise muss der Computer die Aufgaben auf die Kerne verteilen. Wenn die Berechnung also sowieso sehr schnell wäre, kann sie durch Parallelisierung sogar langsamer werden. Wenn der Code aber mindestens ein paar Minuten läuft, ist Parallelisierung oft hilfreich. Üblicherweise ist es sinnvoller, größere Teile des Codes im Ganzen zu parallelisieren, statt einzelne kleine Funktionen jeweils separat zu parallelisieren. Dabei darf man aber auch nicht den Arbeitsspeicher vergessen - mehr dazu später im folgenden Abschnitt.

Implementierung

Nachdem der potenzielle Nutzen der Parallelisierung nun ausführlich besprochen wurde, kommen wir nun zur tatsächlichen Umsetzung. Die genaue Implementierung hängt vom Betriebssystem ab, denn auf Windows-Geräten funktioniert Parallelisierung generell etwas anders als auf Linux oder Mac.

Linux und Mac

Unter Linux, Mac oder anderen Unix-basierten Systemen ist Parallelisierung extrem einfach. Der Code muss lediglich eine lapply-Struktur enthalten. Diesen lapply ersetzt man dann mit der parallelisierten Version mclapply (mc steht für multi-core). Standardmäßig verwendet die mclapply-Funktion alle Kerne, die sie finden kann, aber mit dem Argument mc.cores kann man die Anzahl verwendeter Kerne auch selbst festlegen. Hier ein Beispiel, in dem dieselbe Aktion mit fünf verschiedenen Dataframes (dat1 bis dat5) ausgeführt wird:

library(parallel)

dataList <- list(dat1, dat2, dat3, dat4, dat5)

mclapply(dataList, mc.cores = 5, function(dat) {
  # Code, der irgendetwas auf dat berechnet
})

Der Code gibt dann eine Liste der Länge fünf zurück, die die Ergebnisse für die fünf Dataframes enthält, also ganz genau wie beim lapply - nur schneller.

Windows

Auf Windows ist Parallelisierung ein wenig komplizierter. Auch hier kann man das Paket parallel verwenden. Zunächst muss mit der Funktion makeCluster ein sogenanntes "Cluster" eingerichtet werden, das eine bestimmte Anzahl von "Nodes" enthält. Jede Node verwendet einen Kern und kann eine Aufgabe übernehmen. Für die eigentlichen Berechnungen verwendet man dann parLapply, und am Ende stoppt man das Cluster mit stopCluster.

Im folgenden Beispiel werden unterschiedlich große Gruppen von Zufallszahlen gezogen und darauf Quantile berechnet:

library(parallel)

myCluster <- makeCluster(4) # Cluster mit 4 Nodes

result <- parLapply(myCluster,
                    seq(1000, 100000, by = 1000),
                    function(x) {
                      # Funktion, die irgendetwas simuliert:
                      y <- rnorm(n = x)
                      quantile(y)
                    })

stopCluster(myCluster)

Abgesehen von dem etwas komplizierten Code gibt es noch einen Grund, warum sich Parallelisierung auf Windows etwas seltener lohnt: den Arbeitsspeicher. Unter Linux/Mac kann jeder Prozess auf denselben Bereich des Arbeitsspeichers zugreifen. Das heißt, wenn die Berechnungen auf einem sehr großen Datensatz basieren, kann jeder Prozess auf dieselbe eine Kopie dieses Datensatzes zugreifen. Erst für Zwischenschritte wird weiterer Arbeitsspeicher benötigt. Unter Windows hingegen braucht jeder Prozess seine eigene Kopie des Datensatzes. Auf diese Weise kann schon direkt am Anfang ein großer Teil des Arbeitsspeichers belegt sein.

Ein weiterer Unterschied ist die Umgebung, in der die Berechnungen ausgeführt werden. Unter Linux hat der parallelisierte Code Zugriff auf alle R-Objekte in der aktuellen Umgebung und auf die Funktionen aller geladenen Pakete. Unter Windows hingegen startet jeder Prozess in einer neuen, leeren Umgebung. Deswegen muss jedes benötigte Objekt "von Hand" an diese Umgebung weitergereicht werden. Hierfür verwendet man vor dem Aufruf von parLapply die Funktion clusterExport (Details unter ?clusterExport).

Wie viele Kerne sollte man verwenden?

Die optimale Anzahl von Kernen hängt von mehreren Aspekten ab. Wenn man den Computer parallel noch für andere Aufgaben verwenden möchte (z.B. E-Mails checken oder ein Textdokument bearbeiten), sollte man dafür natürlich mindestens einen Kern reservieren. Ein weiterer Punkt ist der schon erwähnte Arbeitsspeicher: Wenn mehrere Prozesse parallel laufen, braucht natürlich jeder Prozess seinen eigenen Arbeitsspeicher, um beispielsweise Zwischenergebnisse zu speichern. Häufig ist nicht die Anzahl der Kerne der begrenzende Faktor bei der Parallelisierung, sondern der Arbeitsspeicher.

Weitere Teile der Artikelserie über Code-Performanz in R:

Zurück