术语“向量化”在不同上下文中是否意味着不同的事物?

时间:2018-08-04 02:43:28

标签: r julia vectorization

根据我之前所读的内容,矢量化是一种并行化形式,称为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会对代码进行反矢量化,为什么加号呢?

2 个答案:

答案 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 RrowSums(A * B)是R中的“向量化”,因为"*"rowSums都以C语言编码为一个循环。但是,R无法将它们融合到一个C循环中,以避免将临时矩阵C = A * B生成到RAM中。

另一个例子是R的回收规则或任何依赖该规则的计算。例如,当您将a的标量A添加到矩阵A + a时,实际发生的情况是a首先被复制为矩阵BA具有相同维度,即B <- matrix(a, nrow(A), ncol(A)),则计算两个矩阵之间的加法:A + B。显然,不希望生成临时矩阵B,但是遗憾的是,除非为A + a编写自己的C函数并在R中调用它,否则您无法做得更好。这称为>“ Bogumił Kamiński's answer中的“只有在明确实现的情况下这种融合才有可能。”

为了处理许多临时结果的记忆效应,R具有称为“垃圾收集”的复杂机制。它有帮助,但是如果您在代码中的某个地方生成了一些非常大的临时结果,内存仍然会爆炸。函数outer是一个很好的例子。使用此功能,我已经写了很多答案,但是它特别不便于记忆。

当我开始讨论“向量化”的副作用时,我可能在本次编辑中脱节。小心使用。

  • 记住内存使用情况;可能会有一个内存效率更高的矢量化实现。例如,如在两个矩阵之间的行式点积的链接线程中所述,c(crossprod(x, y))sum(x * y)更好。
  • 准备使用已编译代码的CRAN R软件包。如果在R中发现现有的矢量化函数只能完成您的任务,请在CRAN中寻找可能的R软件包来完成此任务。您可以在Stack Overflow上遇到编码瓶颈的问题,有人可能会指出您在正确的程序包中的正确功能。
  • 很高兴编写自己的编译代码。

答案 1 :(得分:6)

我认为值得一提的是,您所指的帖子并未涵盖Julia中所有当前的矢量化功能。

重要的是,Julia中的矢量化是在Julia中实现的,而不是R,后者是在语言之外实现的。这篇文章https://julialang.org/blog/2017/01/moredots对此进行了详细说明。

Julia可以将广播操作的任何序列融合到单个循环中的结果。在提供向量化的其他语言中,只有明确实现,这种融合才有可能。

总结:

  1. 在Julia中,您可以期望矢量化代码像循环一样快。
  2. 如果执行一系列矢量化运算,则通常可以期望Julia快于R,因为它可以避免分配中间的计算结果。

编辑:

下面李哲源的注释是一个示例,该示例表明,如果您想将向量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

我们可以看到它不仅可以节省内存,而且可以更快地执行代码。