因此,我们习惯于向每个R新用户说" apply
没有矢量化,请查看Patrick Burns R Inferno Circle 4 "其中说(我引用):
常见的反射是使用apply系列中的函数。 这不是 矢量化,它是循环隐藏。 apply函数有一个for循环 它的定义。 lapply函数掩盖循环,但执行 时间往往大致等于明确的for循环。
确实,快速查看apply
源代码会显示循环:
grep("for", capture.output(getAnywhere("apply")), value = TRUE)
## [1] " for (i in 1L:d2) {" " else for (i in 1L:d2) {"
好了到目前为止,但看看lapply
或vapply
实际上会发现完全不同的图片:
lapply
## function (X, FUN, ...)
## {
## FUN <- match.fun(FUN)
## if (!is.vector(X) || is.object(X))
## X <- as.list(X)
## .Internal(lapply(X, FUN))
## }
## <bytecode: 0x000000000284b618>
## <environment: namespace:base>
所以显然没有R for
循环隐藏在那里,而是调用内部C编写的函数。
此外,让我们以colMeans
函数为例,该函数从未被指责为未被矢量化
colMeans
# function (x, na.rm = FALSE, dims = 1L)
# {
# if (is.data.frame(x))
# x <- as.matrix(x)
# if (!is.array(x) || length(dn <- dim(x)) < 2L)
# stop("'x' must be an array of at least two dimensions")
# if (dims < 1L || dims > length(dn) - 1L)
# stop("invalid 'dims'")
# n <- prod(dn[1L:dims])
# dn <- dn[-(1L:dims)]
# z <- if (is.complex(x))
# .Internal(colMeans(Re(x), n, prod(dn), na.rm)) + (0+1i) *
# .Internal(colMeans(Im(x), n, prod(dn), na.rm))
# else .Internal(colMeans(x, n, prod(dn), na.rm))
# if (length(dn) > 1L) {
# dim(z) <- dn
# dimnames(z) <- dimnames(x)[-(1L:dims)]
# }
# else names(z) <- dimnames(x)[[dims + 1]]
# z
# }
# <bytecode: 0x0000000008f89d20>
# <environment: namespace:base>
咦?它也只是调用.Internal(colMeans(...
,我们也可以在rabbit hole中找到它。那么这与.Internal(lapply(..
?
实际上,一个快速的基准测试表明sapply
的表现不比colMeans
差,而且对于大数据集来说比for
循环要好得多
m <- as.data.frame(matrix(1:1e7, ncol = 1e5))
system.time(colMeans(m))
# user system elapsed
# 1.69 0.03 1.73
system.time(sapply(m, mean))
# user system elapsed
# 1.50 0.03 1.60
system.time(apply(m, 2, mean))
# user system elapsed
# 3.84 0.03 3.90
system.time(for(i in 1:ncol(m)) mean(m[, i]))
# user system elapsed
# 13.78 0.01 13.93
换句话说,lapply
和vapply
实际上是向量化是否正确(与apply
相比,for
也称为lapply
)的循环以及帕特里克伯恩斯真正想说的是什么?
答案 0 :(得分:72)
首先,在您的示例中,您将对&#34; data.frame&#34;进行测试。这对colMeans
,apply
和"[.data.frame"
不公平,因为它们有开销:
system.time(as.matrix(m)) #called by `colMeans` and `apply`
# user system elapsed
# 1.03 0.00 1.05
system.time(for(i in 1:ncol(m)) m[, i]) #in the `for` loop
# user system elapsed
# 12.93 0.01 13.07
在矩阵上,图片有点不同:
mm = as.matrix(m)
system.time(colMeans(mm))
# user system elapsed
# 0.01 0.00 0.01
system.time(apply(mm, 2, mean))
# user system elapsed
# 1.48 0.03 1.53
system.time(for(i in 1:ncol(mm)) mean(mm[, i]))
# user system elapsed
# 1.22 0.00 1.21
回到问题的主要部分,lapply
/ mapply
/ etc与简单的R-loop之间的主要区别在于循环完成的地方。正如Roland所说,C和R循环都需要在每次迭代中评估R函数,这是最昂贵的。真正快速的C函数是那些在C中完成所有事情的函数,因此,我想,这应该是&#34; vectorised&#34;是关于?
我们在每个&#34; list&#34;元素中找到平均值的示例:
(编辑5月11日&#39; 16 :我相信找到&#34;意思&#34;的例子不是评估R之间差异的良好设置迭代和编译代码的功能,(1)由于R&#34;数字&#34; s在简单sum(x) / length(x)
上的特殊性和(2)它应该更有意义进行测试on&#34; list&#34; s with length(x) >> lengths(x)
。所以,&#34; mean&#34;示例移到最后并替换为另一个。)
作为一个简单的例子,我们可以考虑找到&#34;列表中每个length == 1
元素的相反情况&#34;:
在tmp.c
文件中:
#include <R.h>
#define USE_RINTERNALS
#include <Rinternals.h>
#include <Rdefines.h>
/* call a C function inside another */
double oppC(double x) { return(ISNAN(x) ? NA_REAL : -x); }
SEXP sapply_oppC(SEXP x)
{
SEXP ans = PROTECT(allocVector(REALSXP, LENGTH(x)));
for(int i = 0; i < LENGTH(x); i++)
REAL(ans)[i] = oppC(REAL(VECTOR_ELT(x, i))[0]);
UNPROTECT(1);
return(ans);
}
/* call an R function inside a C function;
* will be used with 'f' as a closure and as a builtin */
SEXP sapply_oppR(SEXP x, SEXP f)
{
SEXP call = PROTECT(allocVector(LANGSXP, 2));
SETCAR(call, install(CHAR(STRING_ELT(f, 0))));
SEXP ans = PROTECT(allocVector(REALSXP, LENGTH(x)));
for(int i = 0; i < LENGTH(x); i++) {
SETCADR(call, VECTOR_ELT(x, i));
REAL(ans)[i] = REAL(eval(call, R_GlobalEnv))[0];
}
UNPROTECT(2);
return(ans);
}
在R方面:
system("R CMD SHLIB /home/~/tmp.c")
dyn.load("/home/~/tmp.so")
有数据:
set.seed(007)
myls = rep_len(as.list(c(NA, runif(3))), 1e7)
#a closure wrapper of `-`
oppR = function(x) -x
for_oppR = compiler::cmpfun(function(x, f)
{
f = match.fun(f)
ans = numeric(length(x))
for(i in seq_along(x)) ans[[i]] = f(x[[i]])
return(ans)
})
基准:
#call a C function iteratively
system.time({ sapplyC = .Call("sapply_oppC", myls) })
# user system elapsed
# 0.048 0.000 0.047
#evaluate an R closure iteratively
system.time({ sapplyRC = .Call("sapply_oppR", myls, "oppR") })
# user system elapsed
# 3.348 0.000 3.358
#evaluate an R builtin iteratively
system.time({ sapplyRCprim = .Call("sapply_oppR", myls, "-") })
# user system elapsed
# 0.652 0.000 0.653
#loop with a R closure
system.time({ forR = for_oppR(myls, "oppR") })
# user system elapsed
# 4.396 0.000 4.409
#loop with an R builtin
system.time({ forRprim = for_oppR(myls, "-") })
# user system elapsed
# 1.908 0.000 1.913
#for reference and testing
system.time({ sapplyR = unlist(lapply(myls, oppR)) })
# user system elapsed
# 7.080 0.068 7.170
system.time({ sapplyRprim = unlist(lapply(myls, `-`)) })
# user system elapsed
# 3.524 0.064 3.598
all.equal(sapplyR, sapplyRprim)
#[1] TRUE
all.equal(sapplyR, sapplyC)
#[1] TRUE
all.equal(sapplyR, sapplyRC)
#[1] TRUE
all.equal(sapplyR, sapplyRCprim)
#[1] TRUE
all.equal(sapplyR, forR)
#[1] TRUE
all.equal(sapplyR, forRprim)
#[1] TRUE
(遵循平均发现的原始示例):
#all computations in C
all_C = inline::cfunction(sig = c(R_ls = "list"), body = '
SEXP tmp, ans;
PROTECT(ans = allocVector(REALSXP, LENGTH(R_ls)));
double *ptmp, *pans = REAL(ans);
for(int i = 0; i < LENGTH(R_ls); i++) {
pans[i] = 0.0;
PROTECT(tmp = coerceVector(VECTOR_ELT(R_ls, i), REALSXP));
ptmp = REAL(tmp);
for(int j = 0; j < LENGTH(tmp); j++) pans[i] += ptmp[j];
pans[i] /= LENGTH(tmp);
UNPROTECT(1);
}
UNPROTECT(1);
return(ans);
')
#a very simple `lapply(x, mean)`
C_and_R = inline::cfunction(sig = c(R_ls = "list"), body = '
SEXP call, ans, ret;
PROTECT(call = allocList(2));
SET_TYPEOF(call, LANGSXP);
SETCAR(call, install("mean"));
PROTECT(ans = allocVector(VECSXP, LENGTH(R_ls)));
PROTECT(ret = allocVector(REALSXP, LENGTH(ans)));
for(int i = 0; i < LENGTH(R_ls); i++) {
SETCADR(call, VECTOR_ELT(R_ls, i));
SET_VECTOR_ELT(ans, i, eval(call, R_GlobalEnv));
}
double *pret = REAL(ret);
for(int i = 0; i < LENGTH(ans); i++) pret[i] = REAL(VECTOR_ELT(ans, i))[0];
UNPROTECT(3);
return(ret);
')
R_lapply = function(x) unlist(lapply(x, mean))
R_loop = function(x)
{
ans = numeric(length(x))
for(i in seq_along(x)) ans[i] = mean(x[[i]])
return(ans)
}
R_loopcmp = compiler::cmpfun(R_loop)
set.seed(007); myls = replicate(1e4, runif(1e3), simplify = FALSE)
all.equal(all_C(myls), C_and_R(myls))
#[1] TRUE
all.equal(all_C(myls), R_lapply(myls))
#[1] TRUE
all.equal(all_C(myls), R_loop(myls))
#[1] TRUE
all.equal(all_C(myls), R_loopcmp(myls))
#[1] TRUE
microbenchmark::microbenchmark(all_C(myls),
C_and_R(myls),
R_lapply(myls),
R_loop(myls),
R_loopcmp(myls),
times = 15)
#Unit: milliseconds
# expr min lq median uq max neval
# all_C(myls) 37.29183 38.19107 38.69359 39.58083 41.3861 15
# C_and_R(myls) 117.21457 123.22044 124.58148 130.85513 169.6822 15
# R_lapply(myls) 98.48009 103.80717 106.55519 109.54890 116.3150 15
# R_loop(myls) 122.40367 130.85061 132.61378 138.53664 178.5128 15
# R_loopcmp(myls) 105.63228 111.38340 112.16781 115.68909 128.1976 15
答案 1 :(得分:63)
对我来说,矢量化主要是为了让您的代码更容易编写和更容易理解。
矢量化函数的目标是消除与for循环相关的簿记。例如,而不是:
means <- numeric(length(mtcars))
for (i in seq_along(mtcars)) {
means[i] <- mean(mtcars[[i]])
}
sds <- numeric(length(mtcars))
for (i in seq_along(mtcars)) {
sds[i] <- sd(mtcars[[i]])
}
你可以写:
means <- vapply(mtcars, mean, numeric(1))
sds <- vapply(mtcars, sd, numeric(1))
这样可以更轻松地查看相同的内容(输入数据)和不同的内容(您正在应用的功能)。
矢量化的第二个优点是for循环通常用C语言编写,而不是用R语言编写。这具有很大的性能优势,但我并不认为它是矢量化的关键属性。矢量化基本上是为了拯救你的大脑,而不是保存计算机工作。
答案 2 :(得分:46)
我同意Patrick Burns&#39;认为它是循环隐藏而不是代码矢量化。这就是原因:
考虑这个C
代码段:
for (int i=0; i<n; i++)
c[i] = a[i] + b[i]
我们想要做的是什么非常清楚。但如何执行任务或如何执行任务并非如此。默认情况下, for-loop 是一个串行结构。它没有告知是否可以或如何并行完成任务。
最明显的方法是代码以顺序方式运行。将a[i]
和b[i]
加载到寄存器,添加它们,将结果存储在c[i]
中,并为每个i
执行此操作。
但是,现代处理器具有vector or SIMD指令集,当执行相同操作时,它能够在相同指令期间在数据向量上运行(例如, ,添加两个向量,如上所示)。根据处理器/体系结构的不同,可以在同一条指令下添加a
和b
中的四个数字,而不是一次添加一个数字。
我们想利用Single Instruction Multiple Data并执行数据级并行,即一次加载4件事,一次添加4件事,一次存储4件事,例。这是代码矢量化。
请注意,这与代码并行化不同 - 其中多个计算同时执行。
如果编译器识别出这样的代码块并且自动对它们进行矢量化,那就太棒了,这是一项艰巨的任务。 Automatic code vectorisation是计算机科学中具有挑战性的研究课题。但随着时间的推移,编译器已经变得更好了。您可以查看GNU-gcc
here的自动矢量化功能。同样适用于LLVM-clang
here。您还可以在最后一个链接中找到与gcc
和ICC
(英特尔C ++编译器)进行比较的基准测试。
gcc
(我在v4.9
上)例如,我不会在-O2
级别优化时自动向量化代码。因此,如果我们要执行上面显示的代码,它将按顺序运行。这是添加两个长度为5亿的整数向量的时间。
我们需要添加标记-ftree-vectorize
或将优化更改为级别-O3
。 (请注意,-O3
也会执行other additional optimisations)。标志-fopt-info-vec
非常有用,因为它可以在循环成功进行矢量化时通知。
# compiling with -O2, -ftree-vectorize and -fopt-info-vec
# test.c:32:5: note: loop vectorized
# test.c:32:5: note: loop versioned for vectorization because of possible aliasing
# test.c:32:5: note: loop peeled for vectorization to enhance alignment
这告诉我们该函数是矢量化的。以下是在长度为5亿的整数向量上比较非向量化和向量化版本的时序:
x = sample(100L, 500e6L, TRUE)
y = sample(100L, 500e6L, TRUE)
z = vector("integer", 500e6L) # result vector
# non-vectorised, -O2
system.time(.Call("Csum", x, y, z))
# user system elapsed
# 1.830 0.009 1.852
# vectorised using flags shown above at -O2
system.time(.Call("Csum", x, y, z))
# user system elapsed
# 0.361 0.001 0.362
# both results are checked for identicalness, returns TRUE
可以安全地跳过此部分而不会失去连续性。
编译器并不总是有足够的信息来进行矢量化。我们可以使用OpenMP specification for parallel programming,它还提供了一个 simd 编译器指令来指示编译器对代码进行矢量化。必须确保没有内存重叠,竞争条件等。手动向量化代码时,否则会导致错误的结果。
#pragma omp simd
for (i=0; i<n; i++)
c[i] = a[i] + b[i]
通过这样做,我们特别要求编译器将其矢量化,无论如何。我们需要使用编译时标志-fopenmp
来激活OpenMP扩展。通过这样做:
# timing with -O2 + OpenMP with simd
x = sample(100L, 500e6L, TRUE)
y = sample(100L, 500e6L, TRUE)
z = vector("integer", 500e6L) # result vector
system.time(.Call("Cvecsum", x, y, z))
# user system elapsed
# 0.360 0.001 0.360
太棒了!这是用gcc v6.2.0和llvm clang v3.9.0(都是通过自制软件安装,MacOS 10.12.3安装)测试的,两者都支持OpenMP 4.0。
从这个意义上说,即使Wikipedia page on Array Programming提到在整个数组上运行的语言通常将其称为矢量化操作,它实际上也是循环隐藏 IMO(除非它实际上是矢量化的)。
如果是R,则C中的rowSums()
或colSums()
代码甚至不会利用代码矢量化 IIUC;它只是C中的一个循环。lapply()
也是如此。在apply()
的情况下,它在R中。所有这些都是循环隐藏。
简而言之,通过以下方式包装R函数:
只需在
例如,C
中编写 for-loop !=向您的代码进行矢量化。
只需在R
中编写 for-loop !=向您的代码进行矢量化。Intel Math Kernel Library (MKL)实现了向量化形式的函数。
HTH
参考文献:
答案 3 :(得分:35)
所以把好的答案/评论总结成一些一般的答案并提供一些背景:R有4种类型的循环(从非矢量化到矢量化顺序)
for
循环,在每次迭代中重复调用R函数(未向量化) 所以*apply
系列是第二种类型。 apply
除外,它更像是第一种类型
您可以通过source code
中的评论来理解这一点/ * .Internal(lapply(X,FUN))* /
/ *这是一个特殊的.Internal,所以有未评估的参数。是 从闭包装中调用,所以X和FUN是承诺。乐趣必须 没有评估用于例如bquote。 * /
这意味着lapply
的C代码接受来自R的未评估函数,稍后在C代码本身内对其进行评估。这基本上是lapply
s .Internal
调用
.Internal(lapply(X, FUN))
其中FUN
参数包含R函数
不的colMeans
.Internal
调用FUN
参数
.Internal(colMeans(Re(x), n, prod(dn), na.rm))
colMeans
,与lapply
不同,确切需要使用哪个函数,因此它会在C代码内部计算平均值。
您可以在lapply
C code
for(R_xlen_t i = 0; i < n; i++) {
if (realIndx) REAL(ind)[0] = (double)(i + 1);
else INTEGER(ind)[0] = (int)(i + 1);
tmp = eval(R_fcall, rho); // <----------------------------- here it is
if (MAYBE_REFERENCED(tmp)) tmp = lazy_duplicate(tmp);
SET_VECTOR_ELT(ans, i, tmp);
}
总结一下, lapply
没有矢量化,虽然它比普通的R for
循环有两个可能的优势
在循环中访问和分配似乎在C中更快(即在lapply
函数中)虽然差异看起来很大,但我们仍然保持在微秒级别并且代价高昂的是每次迭代中R函数的估值。一个简单的例子:
ffR = function(x) {
ans = vector("list", length(x))
for(i in seq_along(x)) ans[[i]] = x[[i]]
ans
}
ffC = inline::cfunction(sig = c(R_x = "data.frame"), body = '
SEXP ans;
PROTECT(ans = allocVector(VECSXP, LENGTH(R_x)));
for(int i = 0; i < LENGTH(R_x); i++)
SET_VECTOR_ELT(ans, i, VECTOR_ELT(R_x, i));
UNPROTECT(1);
return(ans);
')
set.seed(007)
myls = replicate(1e3, runif(1e3), simplify = FALSE)
mydf = as.data.frame(myls)
all.equal(ffR(myls), ffC(myls))
#[1] TRUE
all.equal(ffR(mydf), ffC(mydf))
#[1] TRUE
microbenchmark::microbenchmark(ffR(myls), ffC(myls),
ffR(mydf), ffC(mydf),
times = 30)
#Unit: microseconds
# expr min lq median uq max neval
# ffR(myls) 3933.764 3975.076 4073.540 5121.045 32956.580 30
# ffC(myls) 12.553 12.934 16.695 18.210 19.481 30
# ffR(mydf) 14799.340 15095.677 15661.889 16129.689 18439.908 30
# ffC(mydf) 12.599 13.068 15.835 18.402 20.509 30
如@Roland所述,它运行编译的C循环而不是解释的R循环
虽然在对代码进行矢量化时,您需要考虑一些事项。
df
)属于data.frame
类,则会使用一些向量化的函数(例如colMeans
,colSums
,{{ 1}}等等必须首先将其转换为矩阵,因为这是它们的设计方式。这意味着对于大rowSums
这可能会产生巨大的开销。虽然df
不必这样做,因为它从lapply
中提取实际向量(因为df
只是一个向量列表),因此,如果你没有这样做许多列但很多行,data.frame
有时可能比lapply(df, mean)
更好。colMeans(df)
和泛型(.Primitive
,S3
)请参阅here附加信息。通用函数必须执行方法调度,有时这是一种代价高昂的操作。例如,S4
是通用mean
函数,而S3
是sum
。因此,与上面列出的原因{/ 1}相比,有些时候Primitive
可能非常有效{/ 1}
醇>