根据我之前所读的内容,矢量化是一种并行化形式,称为SIMD。它允许处理器在数组上同时执行相同的指令(例如加法)。
但是,在阅读The Relationship between Vectorized and Devectorized Code时,我对Julia和R的向量化性能感到困惑。该帖子声称,经过向量化的Julia代码(通过循环)比在Julia和R中的向量化代码都要快,因为:
这会使一些对内部结构不熟悉的人感到困惑 R。因此,值得注意的是如何提高R代码的速度。 性能提升的过程非常简单:一开始 使用去矢量化的R代码,然后将其替换为矢量化的R代码,并 然后最终在去矢量化的C代码中实现此矢量化的R代码。 遗憾的是,对于许多R用户来说,最后一步是看不见的, 因此,将向量化本身视为一种增加机制 性能。向量化本身并不能帮助提高代码速度。什么 使R中的向量化有效是因为它提供了一种 将计算移到C中,其中隐藏了去矢量化层 可以发挥魔力。
它声称R将用R编写的矢量化代码转换为C中的反矢量化代码。如果矢量化更快(作为并行化的一种形式),为什么R会对代码进行反矢量化,为什么加号呢?
答案 0 :(得分:13)
R中的“向量化”是R解释器视图中的向量处理。以函数cumsum
为例。进入时,R解释器看到向量x
已传递到此函数中。但是,随后将工作传递给R解释器无法分析/跟踪的C语言。 C在工作时,R在等待。在R的解释器恢复工作时,已经处理了向量。因此,在R看来,它发出了一条指令,但处理了一个向量。这类似于SIMD的概念-“单指令,多数据”。
在R中,不仅将cumsum
函数接受一个向量并返回一个向量,还将其视为“向量化”,将sum
之类的函数接受一个向量并返回一个标量也被称为“向量化”。
简而言之:每当R为循环调用一些已编译的代码时,它就是“向量化”。如果您想知道为什么这种“向量化”有用,那是因为由编译语言编写的循环比以解释语言编写的循环要快。 C循环被翻译成CPU可以理解的机器语言。但是,如果CPU要执行R循环,则需要R的解释器的帮助来逐次读取。这就像,如果您知道中文(最难的人类语言),您可以更快地回应说中文的人。否则,您需要翻译人员先用英语在句子中翻译中文,然后再用英语回复,然后翻译人员逐句将其翻译回中文。沟通的效率大大降低。
x <- runif(1e+7)
## R loop
system.time({
sumx <- 0
for (x0 in x) sumx <- sumx + x0
sumx
})
# user system elapsed
# 1.388 0.000 1.347
## C loop
system.time(sum(x))
# user system elapsed
# 0.032 0.000 0.030
请注意,R中的“矢量化”只是SIMD的一种类比,而不是真正的类比。真正的SIMD使用CPU的向量寄存器进行计算,因此是通过数据并行性进行的真正的并行计算。 R不是可以编程CPU寄存器的语言;为此,您必须编写编译后的代码或汇编代码。
R的“向量化”并不关心如何真正执行以编译语言编写的循环。毕竟这是R口译员所不具备的。关于这些编译后的代码是否将通过SIMD执行,请阅读Does R leverage SIMD when doing vectorized calculations?
有关R中“矢量化”的更多信息
我不是Julia用户,但是Bogumił Kamiński展示了该语言的令人印象深刻的功能:循环融合。朱莉娅可以做到这一点,因为正如他指出的那样,“朱莉娅中的矢量化是在朱莉娅中实现的”,而不是在语言之外。
这揭示了R的向量化的缺点:速度通常是以内存使用为代价的。我并不是说Julia不会遇到这个问题(因为我不使用它,我也不知道),但是对于R来说绝对是正确的。
这里是一个示例:Fastest way to compute row-wise dot products between two skinny tall matrices in R。 rowSums(A * B)
是R中的“向量化”,因为"*"
和rowSums
都以C语言编码为一个循环。但是,R无法将它们融合到一个C循环中,以避免将临时矩阵C = A * B
生成到RAM中。
另一个例子是R的回收规则或任何依赖该规则的计算。例如,当您将 为了处理许多临时结果的记忆效应,R具有称为“垃圾收集”的复杂机制。它有帮助,但是如果您在代码中的某个地方生成了一些非常大的临时结果,内存仍然会爆炸。函数 当我开始讨论“向量化”的副作用时,我可能在本次编辑中脱节。小心使用。a
的标量A
添加到矩阵A + a
时,实际发生的情况是a
首先被复制为矩阵B
与A
具有相同维度,即B <- matrix(a, nrow(A), ncol(A))
,则计算两个矩阵之间的加法:A + B
。显然,不希望生成临时矩阵B
,但是遗憾的是,除非为A + a
编写自己的C函数并在R中调用它,否则您无法做得更好。这称为>“ Bogumił Kamiński's answer中的“只有在明确实现的情况下这种融合才有可能。”
outer
是一个很好的例子。使用此功能,我已经写了很多答案,但是它特别不便于记忆。
c(crossprod(x, y))
比sum(x * y)
更好。
答案 1 :(得分:6)
我认为值得一提的是,您所指的帖子并未涵盖Julia中所有当前的矢量化功能。
重要的是,Julia中的矢量化是在Julia中实现的,而不是R,后者是在语言之外实现的。这篇文章https://julialang.org/blog/2017/01/moredots对此进行了详细说明。
Julia可以将广播操作的任何序列融合到单个循环中的结果。在提供向量化的其他语言中,只有明确实现,这种融合才有可能。
总结:
编辑:
下面李哲源的注释是一个示例,该示例表明,如果您想将向量x
的所有元素增加1
,Julia可以避免任何分配:
julia> using BenchmarkTools
julia> x = rand(10^6);
julia> @benchmark ($x .+= 1)
BenchmarkTools.Trial:
memory estimate: 0 bytes
allocs estimate: 0
--------------
minimum time: 819.230 μs (0.00% GC)
median time: 890.610 μs (0.00% GC)
mean time: 929.659 μs (0.00% GC)
maximum time: 2.802 ms (0.00% GC)
--------------
samples: 5300
evals/sample: 1
在代码.+=
中执行加法操作(仅在基准测试之前在表达式前面添加$
,在常规代码中为x .+= 1
)。而且我们看到没有完成内存分配。
如果将其与R中的可能实现进行比较:
> library(microbenchmark)
> x <- runif(10^6)
> microbenchmark(x <- x + 1)
Unit: milliseconds
expr min lq mean median uq max neval
x <- x + 1 2.205764 2.391911 3.999179 2.599051 5.061874 30.91569 100
我们可以看到它不仅可以节省内存,而且可以更快地执行代码。