Skip to main content
 首页 » 编程设计

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

2024年06月03日45qq号

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

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

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

请您参考如下方法:

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

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

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

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

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

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

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

然后,当然,还有多线程——在(至少在逻辑上)独立的处理器/内核上运行多个指令流。

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

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

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