How to make your code faster: Byte-Compile

A la reunió del març del R user Group Barcelona vaig preparar uns quants scripts i proves amb el package compiler. Faig aquesta entrada com a resum del que vaig comentar en aquella aquella xerrada.

Petita introducció al package:

El package compile va apareixer a la versió R.2.13 directament com un package per defecte, no te l’havies de baixar del CRAN. A la versió de l’R.2.14 el package base i els packages recomanats estan byte-compilats per defecte.

Però, què vol dir byte-compilate? Tot i no ser un expert en la matèria, i ajudant-me d’Internet, una de les  manera de distingir els llenguatges de programació són: els que són compilats i els que són interpretats.  Exemples de llenguatges compilats són el C, Fortran, COBOL… i llenguatges interpretats l’R, Python, Perl, Ruby…  El byte-compile consisteix a una traduir el llenguatge interpretat a un llenguatge més senzill i orientat a la màquina permetent així una millora en el rendiment del codi. La representació interna d’aquest llenguatge senzill és una cadena de bytes, d’aquí el nom.

El package compiler precisament el que fa es posar el nostre codi en R traduir-lo a bytecode. Això ho podem fer, utilitzant la funció cmpfun, aplicant-la sobre una funció programada per tu.

Per exemple, si generem la funció f ( ineficient per construcció) i després li apliquem la funció del compiler cmpfun

f <- function(n){
 x = 0
 for( i in 1:n){
 x = x + 1/i
 }
 return(x)
}

fc <- cmpfun(f)

Obtenim els següents resultats:

>system.time(f(1e6))

user system elapsed

0.989 0.001 0.990

system.time(fc(1e6))

user system elapsed

0.154 0.000 0.154

En els casos reals no millora tant, sobretot si la teva funció ja està optimitzada o si utilitzes funcions que l’R les té compilades en C o Fortran. La gran avantatge és que per lo poc que costa de provar-ho, val la pena aplicar-ho i comparar la velocitat, habitualment obtens millores d’entre un 20% i un 30%.

Principals diferències utilitzant el package compiler entre l’R.2.13 i l’R2.14

Una de les coses curioses que vaig descobrir al preparar-me la xerrada va ser que la versió de l’R.2.14 era molt més ràpida que la R.2.13 per aquest motiu. Suposo que la majoria ja us haureu passat a la R.2.14 o a la R.2.15 però si no ho heu fet, penseu que guanyareu una mica en velocitat.

Si una funció crida una altra funció, s’han de compilar les dues?

La resposta és que si. A vegades és a les funcions internes les que li pots treure més rendiment amb el package compiler. Per exemple, utilitzant les funcions f i fc descrites anteriorment, amb les noves que defineixo ara:

g <- function(n){
 x = 0
 for(i in 1:n){
 x = x + f(i)
 }
}

gfc <- function(n){
 x = 0
 for(i in 1:n){
 x = x + fc(i)
 }
}

gc <- cmpfun(g)
gcfc <- cmpfun(gfc)

Obtenim els següents resultats:

> system.time(g(7e3))

user system elapsed

22.078 0.021 22.087

> system.time(gc(7e3))

user system elapsed

22.142 0.020 22.151

> system.time(gfc(7e3))

user system elapsed
3.645 0.018 3.664

> system.time(gcfc(7e3))

user system elapsed

3.613 0.012 3.624

On podem veure que la gran diferència la tenim quan la funció interna està compilada. A vegades, tot i que sembla que hauria de ser més ràpida la versió compliada, pot donar inclús resultats molt lleugerament pitjors, com en el cas de la funció g respecte la gc.

Més informació

L’Autor del package és Luke Tierney, que pertany al R Core, a la seva pàgina web hi ha bastanta informació sobre el package i sobre les futures millores a implementar a l’R perquè sigui més eficient.

 

Lluís Ramon, RUGBCN

Leave a comment