For循环与向量化(Vectorization)

2022-04-14 12:02:17 浏览数 (1)

For循环与向量化(Vectorization)

写在前面

感谢水友们积极的提问,大猫和村长在此再次表示衷心的感谢。通过对水友们问题的汇总,我们发现大多数水友存在一些R语言的应用误区,在此出一期关于该问题的解读。

问题提出

首先思考一个典型的增长率的计算的例子。假设我们有一列时间序列,每个都记录着时刻的值。现在我们希望针对每个计算当期的增长率,其公式如下:

大家可能首先想到的是利用For循环来做。假如一个向量长度为,那么我们就把上面的增长率公式应用遍。这种思路以标量(scalar)的角度考虑问题。这样是否真的有效率?除此之外,能否有其他的思路? ”

解决方法

For循环

首先我们用R语言最底层的For循环进行函数的编写。我们定义向量长度为10000,重复运行1000次,测定计算其运行的中位数时长为了防止极值对结果的影响)。在计算时间方面,运用microbenchmark包,生成每次运行的时间,最后计算中位数,代码与结果如下:

代码语言:javascript复制
library(microbenchmark)
growthRBL <- function(x) {
    growth <- vector(mode = "numeric")
    for (i in seq_along(x)) {
        growth[i] <- x[i]/x[i-1] - 1
    }
    growth
}
time1 <- microbenchmark(growthRBL(1:10000), times = 1000) %>% as.data.table()
time1[, median(time)/1e6]

3.3792015

通过结果可以发现,平均每次运行所需要耗费的时长为3.3秒左右。有没有更快的方法呢?我们来看下面的思路。

Vectorized(向量化)

根据Hadley Wickham在其著作Advanced R中第一章所涉及到的内容,R最底层的数据结构只有两种:向量vector)和列表list),其他所有的数据格式都是通过这两种最基本的数据结构衍化而来。向量作为最基本的数据结构,其在进行底层编写的时候,进行了很大程度的优化设计。向量有时候作为一种基本的编写思路,是具有很高效率的。有鉴于此,我们通过R语言最底层的向量思维进行函数编写。

代码语言:javascript复制
library(microbenchmark)
growthRBV <- function(x) {
    shift <- function(y) {
        c(NA, y[1:(length(y)-1)])
    }
    x/shift(x) - 1
}
time2 <- microbenchmark(growthRBV(1:10000), times = 1000) %>% as.data.table()
time2[, median(time)/1e6]

0.084901

我们在函数中编写了另一个函数,名曰shift由于我们需要做的是向量中某一个元素与前一个元素的处理结果,那么只需要将元素往后进行移位,与原来的向量进行一一对应的处理即可,这样便达到了以向量进行处理的模式。也可称之为向量化vectorization)。

上述的运行结果更能反映这种编写的效率,可以看到运行速度提升了将近40倍,运行时间变成了0.08s左右。

关于For循环和Vectorization的深入思考

Vectorization在更多包的拓展

现在有很多的R包会对底层的一些函数进行优化,也即是对向量化的进一步优化,我们选择效率较为强大的data.table,看看其对shift函数的优化情况。

代码语言:javascript复制
library(microbenchmark)
growthRDV <- function(x) {
    x/data.table::shift(x) - 1
}
time3 <- microbenchmark(growthRDV(1:10000), times = 1000) %>% as.data.table()
time3[, median(time)/1e6]

0.0406515

通过结果可以发现,data.table对于shift函数的优化又使其效率翻倍!!运行时间继续降低至0.04s!!

更底层的For循环

R语言本身的For循环效率相对低下,究其原因在于R作为高级语言,循环本身需要先进行编译,再放入底层进行处理。更为直接的做法,如果想提升效率,则可以直接将循环放入底层进行运行。有鉴于此,C 可作为一种比较好的替代手段。R语言提供了一个很好的C 语言的接口,Rcpp包能够比较方便调用C 的语句进行操作。(若有对Rcpp感兴趣的同学可以戳这里进行了解)

代码语言:javascript复制
library(microbenchmark)
Rcpp::cppFunction('NumericVector growthRCL(NumericVector x){
    int n = x.size();
    NumericVector y(n);
    y[0] = NA_REAL;
    for(int i = 1; i < n; i  ) {
        y[i] = x[i]/x[i-1]-1;
    }
    return y;
}')
time4 <- microbenchmark(growthRCL(1:10000), times = 1000) %>% as.data.table()
time4[, median(time)/1e6]

0.029601

如上所示,使用Rcpp包中的cppFunction进行C 语句的调用。在这里会自动调用已经配置好的C 头文件,并自动编译而后运行。调用的C 语句,在R语言中皆有相对应的数据格式。通过运行结果可以发现,Rcpp调用的底层循环略优于data.table的向量化,运行时间在0.03s左右。

总结

通过上面的运行效率排序可以发现:

我们也可以总结出以下两点:

  1. 在R语言中一般意义上的数据操作,能够向量化尽量进行向量化,For循环尽量避免使用。
  2. 利用data.table进行数据操作有着比R本身向量化更好的效率表现,如果自身对效率的要求更高,可以利用更底层的语言接口进行编写。
  • 最后还有一点需要注意:向量化并不能解决一切问题。当遇到一些特殊情况,比如函数嵌套调用过多,或者数据迭代问题,对更为底层的语言进行调用,则会显得更为有效。

0 人点赞