【问题标题】:Why is vectorization, faster in general, than loops?为什么矢量化通常比循环更快?
【发布时间】:2016-05-07 15:05:01
【问题描述】:

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

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

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

【问题讨论】:

  • 硬件可以并行。您可以在 1 个周期内对两个 32 位数字进行异或运算。您可以在 1 个周期内对两个 1048576 位数字进行异或运算。只需在芯片上再烧几根线即可。
  • 使用现代短向量 SIMD,您可以在循环中使用向量来处理整个数组。老式的 Cray 向量机可以设置一个大操作,然后一条指令会加载/操作/存储,但这不是 x86 SSE / ARM NEON / PowerPC AltiVec 的工作方式。

标签: performance language-agnostic vectorization simd low-level


【解决方案1】:

矢量化有两个主要好处。

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

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

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


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

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

【讨论】:

    【解决方案2】:

    向量化(通常使用的术语)指的是 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 次双精度运算。在最佳矢量化代码上,它比您仅基于其(低得多的)时钟速度所预期的更接近当前 CPU 的速度。不过,充分利用这一点并不总是那么容易(现在仍然不是)。

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

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

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

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


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

    【讨论】:

    • 在我的计算机系统入门课程(以及我们的并行编程课程)中,我们将处理器(或多核 CPU 的单核)视为一种只能执行的黑盒系统事情顺序;不能同时计算任何计算。这是不正确的吗?或者核心是否有自己的子处理器,每个子处理器都可以进行简单的计算?
    • 是的,就现代(相当高端的)处理器而言,这是不正确的。几十年来,主流桌面/服务器 CPU 一直支持各种类型的并行性。纯粹的顺序将是(例如)486,但不再适用于原始的奔腾。在大型机上,同样的事情发生在更早之前(例如,CDC 6500 的架构类似于 Pentium,而 6600 的架构类似于 Pentium Pro)。这些是在 1964 年左右发布的。
    • 大多数现代 CPU 的流水线宽度为 4 uop(Intel 自 Core2,AMD 自 Bulldozer)。如果您混合了加载、存储和单 uop ALU 指令,那么每个时钟为您提供 4 条指令。 (比较+分支指令对可以融合为 1 uop,因此 Haswell 的真正最大 IPC 是每个时钟 6 条指令,但仅说 4 条更现实。)Ryzen 的管道是 6 宽的,但单 uop 指令每时钟只能运行 5 条钟。 (AVX/AVX2 256b 向量解码为 2 uop,可以很好地填满管道。)Core2 不太可能执行 4 IPC,除非在特制循环上,但在 SKL 上是现实的。
    • vfmadd132ps ymm0, ymm1, [rdi] 这样的 load+ALU 指令可以融合到单个 uop 中,因此您有时可以使向量 ALU 饱和并挤压负载以提供新数据,而不会在前端出现瓶颈。例如,我设法构建了一个循环,在 Skylake 上每个时钟运行 7 个未融合域微指令(2 个微融合负载 + ALU、1 个存储(在英特尔上是 2 个微融合到 1 个微融合)和一个比较+分支.agner.org/optimize/blog/read.php?i=415#857.
    【解决方案3】:

    向量化是一种并行处理。它可以让更多的计算机硬件专门用于执行计算,因此计算速度更快。

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

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

    【讨论】:

    • 那么矢量化代码有完全不同的实现?它实际上只是在更多核心之间分配操作?如果是,这是否意味着单核 CPU 不会从矢量化中看到任何好处,或者每个核心中是否有子核心硬件“单元”(因为没有更好的词)仍然有助于加快速度?
    猜你喜欢
    • 2013-05-29
    • 2020-01-03
    • 2014-09-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-02-09
    相关资源
    最近更新 更多