自从R 2.13以来,compiler包就成为了R默认安装的一部分。自R 2.14以来,所有的标准函数与包都被预先编译为机器码,因此得到了2倍或更多的效率提升。Tal Galili的这篇文章就介绍了使用comipler包加速R代码的执行的方法。此文由R客翻译自原文,省略了部分对JIT原理部分的介绍,详细介绍请参考原文。
什么是JIT(Just-In-Time compliation,即时编译)技术?
传统的计算机语言有两种执行方式:静态编译型,即代码执行前先被转换为机器码;解释型,即一边对代码进行编译,一边执行。而JIT是这两种方式的混合,一边编译,一边执行,但同时也对部分已编译的代码进行缓存,以提高执行的效率。
R与JIT
到今天为止,有两个包支持R语言的JIT:jit包(通过Ra支持)与compiler包(R默认支持)。jit包是由 Stephen Milborrow创建的,它可以实现R中循环语句的JIT来提高执行速度。但它必须通过一个特殊的R, “the Ra Extension to R“来执行。通常的R来执行的话就会完全没有效果。这个jit包已经在2011年停止了开发。compiler包是R 2.13以来成为默认安装的一部分,它不能把R直接编译为最底层的机器码,但可以把作为高层语言的复杂的R代码编译为低层的简单的字节码。后者由于去掉了许多耗时的操作,执行效率上要比前者高很多。compiler包大部分代码是用R写成的,少数是用C。
下面介绍compiler包的使用方法。
介绍
在R中可以使用enableJIT()这个方法,或在R启动时把环境变量R_ENABLE_JIT设为一个非负数的值来激活JIT。enableJIT方法的参数与此非负值的意义如下:
- 0 – 关闭JIT
- 1 – 在第一次调用前编译闭包
- 2 – 包括1,并且在闭包被复制时也先行编译
- 3 – 包括2,并且在执行for, while, repeat函数前执行编译
如果base等包未被编译过,那么首次激活JIT时会稍有迟顿。
示例
以下例子是对“?compile”的例子修改而来的。首先我们定义两个函数。
##### Functions #####
is.compile <- function(func)
{
# 这个函数可以让我们知道它是否已经被编译为字节码
#If you have a better idea for how to do this - please let me know...
if(class(func) != "function") stop("You need to enter a function")
last_2_lines <- tail(capture.output(func),2)
any(grepl("bytecode:", last_2_lines)) # returns TRUE if it finds the text "bytecode:" in any of the last two lines of the function's print
}
# lapply的老版本
slow_func <- function(X, FUN, ...) {
FUN <- match.fun(FUN)
if (!is.list(X))
X <- as.list(X)
rval <- vector("list", length(X))
for(i in seq(along = X))
rval[i] <- list(FUN(X[[i]], ...))
names(rval) <- names(X) # keep `names' !
return(rval)
}
# 编译后的版本
require(compiler)
slow_func_compiled <- cmpfun(slow_func)
请注意最后一行,我们是如何手工把这个函数编译为字节码的。
然后,让我们来运行一下这这两个函数(编译的与未编译的)很多次,并记下它们运行所需的时间。
fo <- function() for (i in 1:1000) slow_func(1:100, is.null) fo_c <- function() for (i in 1:1000) slow_func_compiled(1:100, is.null) system.time(fo()) system.time(fo_c()) # > system.time(fo()) # user system elapsed # 0.54 0.00 0.57 # > system.time(fo_c()) # user system elapsed # 0.17 0.00 0.17
从这个结果我们可以看到,编译后的函数给我们带来了3倍左右的性能提升。
那么,如果我们把cmpfun函数应用到fo上呢?
fo_compiled <- cmpfun(fo) system.time(fo_compiled()) # doing this, will not change the speed at all: # user system elapsed # 0.58 0.00 0.58
我们看到cmpfun并没有给我们带来任何性能提升。为什么会这样呢?这是因为slow_func未被编译。
is.compile(slow_func) # [1] FALSE is.compile(fo) # [1] FALSE is.compile(fo_compiled) # [1] TRUE
cmpfun()这个函数只会编译它所包含的函数,而对于更深层次的函数,它就无能为力了。这时enableJIT()函数就可以助一臂之力。
enableJIT(3) system.time(fo()) # user system elapsed # 0.19 0.00 0.18
我们发现fo函数突然变快了。这是因为enableJIT()这个函数把每个函数都转换成了字节码。这时如果我们再检查一下:
is.compile(fo) # [1] TRUE # when it previously was not compiled, only fo_compiled was... is.compile(slow_func) # [1] TRUE # when it previously was not compiled, only slow_func_compiled was..
这就意味着,如果你想尽量减少对代码的改动,并获得性能的提升,你可以通过在执行所有代码前这样做:
require(compiler) enableJIT(3)
顺便提一下,我们可以用”enableJIT(0)”来关闭JIT,但此时已编译过的函数将仍然保持已编译的状态,除非你对它们重新进行定义(运行定义函数的代码)。
英文原文地址:http://www.r-statistics.com/2012/04/speed-up-your-r-code-using-a-just-in-time-jit-compiler/

