自从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/

Tagged with:
 

Switch to our mobile site