为什么矢量化通常比循环更快?

Why is vectorization, faster in general, than loops?

为什么在硬件执行操作的最低级别和涉及的一般底层操作(即:运行 代码时所有编程语言实际实现的通用内容),矢量化通常比循环?

计算机在循环时会做什么而在使用矢量化时不会做什么(我说的是计算机执行的实际计算,而不是程序员编写的内容),或者它有什么不同之处?

我一直无法说服自己为什么差异如此显着。我可能会相信矢量化代码在某处减少了一些循环开销,但计算机仍然必须执行相同数量的操作,不是吗?例如,如果我们将一个大小为 N 的向量乘以一个标量,我们将有 N 次乘法来执行任何一种方式,不是吗?

矢量化是一种并行处理。它使更多的计算机硬件能够专门用于执行计算,因此计算速度更快。

许多数值问题,尤其是偏微分方程的求解,需要对大量的单元、单元或节点进行相同的计算。矢量化并行执行许多 cells/elements/nodes 的计算。

矢量化使用特殊硬件。与多核 CPU 不同,每个并行处理单元都是一个功能齐全的 CPU 核心,矢量处理单元只能执行简单的操作,并且所有单元同时执行相同的操作,同时对一系列数据值(向量)进行操作。

向量化(通常使用的术语)是指 SIMD(单指令、多数据)操作。

从本质上讲,这意味着一条指令对多个操作数并行执行相同的操作。例如,要将一个大小为 N 的向量乘以一个标量,我们称 M 为它可以同时运算的大小为该大小的操作数的数量。如果是这样,那么它需要执行的指令数大约是 N/M,其中(使用纯标量运算)它必须执行 N 个操作。

例如Intel目前的AVX 2指令集使用256位寄存器。这些可用于保存(和操作)一组 4 个 64 位操作数,或 8 个 32 位操作数。

因此,假设您正在处理 32 位单精度实数,这意味着一条指令可以一次执行 8 次运算(在您的情况下为乘法),因此(至少在理论上)您只用N/8条乘法指令就可以完成N次乘法。至少,理论上,这应该允许操作完成速度大约是一次执行一条指令所允许的速度的 8 倍。

当然,确切的好处取决于每条指令支持多少个操作数。 Intel 的第一次尝试只支持 64 位寄存器,所以要同时操作 8 个项目,这些项目每个只能是 8 位。他们目前支持 256 位寄存器,并宣布支持 512 位(他们甚至可能已经在一些高端处理器中提供了这种支持,但至少目前还没有在普通消费类处理器中提供)。委婉地说,充分利用此功能也并非易事。安排指令以便您实际上有 N 个操作数可用并且在正确的时间在正确的位置不一定是一项简单的任务(根本)。

从正确的角度来看,(现在很古老的)Cray 1 正是通过这种方式获得了很多速度。它的向量单元对每组 64 个 64 位寄存器进行操作,因此每个时钟周期可以进行 64 次双精度运算。在最佳矢量化代码上,它比您仅基于其(低得多的)时钟速度所期望的更接近当前 CPU 的速度。充分利用这一点并不总是那么容易(现在仍然不容易)。

但是请记住,矢量化 不是 CPU 可以并行执行操作的唯一方法。还有指令级并行的可能性,它允许单个 CPU(或 CPU 的单个核心)一次执行多个指令。如果指令是加载、存储和 ALU 的混合,大多数现代 CPU 都包含硬件(理论上)每个时钟周期最多执行大约 4 条指令 1。他们平均每个时钟可以相当例行地执行接近 2 条指令,或者在内存不是瓶颈时在经过良好调整的循环中执行更多指令。

然后,当然还有多线程——运行 多个指令流(至少在逻辑上)分开 processors/cores。

因此,现代 CPU 可能有 4 个内核,每个内核每个时钟可以执行 2 个向量乘法,并且每个指令可以对 8 个操作数进行操作。因此,至少在理论上,它每个时钟可以执行 4 * 2 * 8 = 64 次操作。

一些指令有更好或更差的吞吐量。例如,FP 添加吞吐量低于 FMA 或在 Skylake 之前在 Intel 上乘法(每个时钟 1 个矢量而不是 2 个)。但是像 AND 或 XOR 这样的布尔逻辑每个时钟吞吐量有 3 个向量;构建 AND/XOR/OR 执行单元不需要很多晶体管,因此 CPU 可以复制它们。在使用高吞吐量指令时,总流水线宽度(解码并发布到内核的乱序部分的前端)的瓶颈很常见,而不是特定执行单元的瓶颈。


  1. 但是,随着时间的推移 CPU 往往会有更多可用资源,因此这个数字会上升。

矢量化有两个主要好处。

  1. 主要好处是设计用于支持向量指令的硬件通常具有能够在使用向量指令时并行执行多个 ALU 运算的硬件。例如,如果您要求它使用 16 元素向量指令执行 16 次加法,它可能有 16 个加法器可以同时并行执行所有加法。 访问所有这些加法器1 的方法是通过矢量化。使用标量指令,您只需获得 1 个孤独的加法器。

  2. 使用矢量指令通常可以节省一些开销。您以大块的形式加载和存储数据(在最近的一些英特尔 CPU 上一次最多 512 位)并且每次循环迭代都会做更多的工作,因此循环开销通常相对较低 2,并且您需要更少的指令来完成相同的工作,因此 CPU 前端开销更低,等等。

最后,您在 loopsvectorization 之间的二分法很奇怪。当您采用非矢量代码并将其矢量化时,如果之前有循环,通常会以循环结束,如果没有,则不会。 标量(非向量)指令和向量指令之间的比较。


1 或至少 16 个中的 15 个,也许其中一个也用于标量运算。

2 在标量情况下,您可能会以大量循环展开为代价获得类似的循环开销优势。