Jakob Richter

Schnelleres R dank Kompilierung (also ohne apply())

Inspiriert durch diesen Blogbeitrag wollte ich die Vorkompilierung selbst einmal ausprobieren und auch mit dem so hoch gelobten apply() vergleichen um mich von der Effektivität zu überzeugen.

1
2
3
4
5
6
7
8
9
eingabe <- rnorm(1000000,mean=8,sd=9) #das geht noch sehr schnell
 
meineFunktion <- function(){
	ausgabe <- numeric(length=length(eingabe))
	for(i in seq_along(eingabe)){
		ausgabe[i] <- (eingabe[i] - 8)/9
	}
	return(ausgabe)
}

Warum stecken wir das in eine Funktion? Nur Funktionen können kompiliert werden, darum.
Wie messen wir überhaupt die Zeit? Das geht mit der Funktion system.time(). Alles was dort in den Klammern steht wird wie gewöhnt ausgeführt mit dem Unterschied, dass am Ende in der Konsole die Zeit ausgegeben wird, die benötigt wurde.
Probieren wir nun mal alles aus.

12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
system.time(erg1 <- meineFunktion())
#User      System verstrichen
#3.45        0.00        3.47
 
#Nun mal vorkompeliert:
library(compiler) #sollte schon in R enthalten sein.
meineFunktionKompiliert <- cmpfun(meineFunktion)
 
system.time(erg2 <- meineFunktionKompiliert())
#User      System verstrichen
#0.73        0.02        0.75
 
#Und sapply()?
system.time(erg3 <- sapply(eingabe,function(x) (x-8)/9))
#User      System verstrichen
#4.80        0.00        4.80
 
#und richtig?
system.time(erg4 <- (eingabe-8)/9)
#User      System verstrichen
#0.02        0.00        0.01

Schön! Die kompilierte Version ist wirklich um einiges schneller, aber warum zum teufel ist apply() so lahm? Das liegt vor allem daran, dass meineFunktion() noch halbwegs klug programmiert ist.
Zeit geht bei R immer für zwei Ressourcen drauf. Einmal die Rechenzeit (CPU) und einmal für Speicheroperationen (bei R nur RAM). Sonderlich kompliziert ist die Rechnung (x-8)/9 ja nicht. Viel der Zeit geht also für die Speicheroperationen drauf.
Durch die Zeile ausgabe <- numeric(length=length(eingabe)) in der Funktion wurde ein Ausgabevektor erzeugt, der schon Plätze für die 1000000 Ergebnisse der Rechnung bereithält. Die Rechnung geht dann flott durch und schiebt die Ergebnisse in die bereits vorhandenen Speicherplätze.
Würde man folgende beliebte Initialisierung wählen: ausgabe <- NULL bräuchte der Code erheblich länger. Das liegt daran, dass dann für jedes neue Ergebnis intern ein neuer Vektor erzeugt wird, welcher um 1 länger ist, damit das neue Resultat hineinpasst.
Viel Zeit kann also gespart werden, wenn der Ergebnisvektor schon vorher in seiner kompletten länge definiert wird. Außerdem wirkt cmpfun() besonders dann effektiv, wenn viele arithmetische Berechnungen durchgeführt werden. Greift man in der zu kompilierenden Funktion ausschließlich auf R-Funktionen zurück wird der Speedup nicht besonders groß ausfallen.

Leave a Reply