什么是“向量化”?

318

我在Matlab、Fortran或其他一些编程语言中多次遇到了“向量化”这个术语,但我从未找到过它的解释以及它的作用。因此,我在这里问一下,“向量化”是什么意思?例如,“循环被向量化”是什么意思?


3
@geoffspear 这个链接似乎已经移动到https://en.wikipedia.org/wiki/Array_programming。 - I Like to Code
向量化意味着什么? - phuclv
9个回答

361

许多CPU都有“向量”或“SIMD”指令集,可以同时对两个、四个或更多数据块应用相同的操作。现代x86芯片具有SSE指令,许多PPC芯片具有“Altivec”指令,甚至一些ARM芯片也有一个向量指令集,称为NEON。

“向量化”(简化)是将循环重写的过程,使其处理数组的4个元素(例如)而不是单独处理一个元素N次,同时进行N/4次。

我选择4是因为现代硬件最有可能直接支持32位浮点数或整数。


向量化和循环展开的区别: 考虑下面这个非常简单的循环,它将两个数组的元素相加并将结果存储到第三个数组中。

for (int i=0; i<16; ++i)
    C[i] = A[i] + B[i];

展开这个循环会将其转换成类似以下的形式:

for (int i=0; i<16; i+=4) {
    C[i]   = A[i]   + B[i];
    C[i+1] = A[i+1] + B[i+1];
    C[i+2] = A[i+2] + B[i+2];
    C[i+3] = A[i+3] + B[i+3];
}

然而,将其向量化会产生类似于这样的结果:

for (int i=0; i<16; i+=4)
    addFourThingsAtOnceAndStoreResult(&C[i], &A[i], &B[i]);

在这里,“addFourThingsAtOnceAndStoreResult”是一个占位符,用于指定编译器使用的任何内置向量指令。


术语:

请注意,现代大多数静态编译器能够自动向量化非常简单的循环,例如此处的循环,这通常可以通过编译选项启用(在现代C和C++编译器中,如gcc -O3 -march=native,默认情况下开启)。 OpenMP #pragma omp simd有时对提示编译器有帮助,特别是对于“reduction”循环,例如对FP数组求和,在这种情况下,向量化需要假装FP数学是可结合的。

更复杂的算法仍然需要程序员的帮助才能生成良好的向量代码;我们称之为手动矢量化,通常使用类似x86 _mm_add_ps的内在函数,它们映射到单个机器指令,如SIMD prefix sum on Intel cpuHow to count character occurrences using SIMD。甚至可以使用SIMD解决短小的非循环问题,如Most insanely fastest way to convert 9 char digits into an int or unsigned intHow to convert a binary integer number to a hex string?

“向量化”这个术语也用于描述更高级的软件转换,其中您可能会完全抽象掉循环并仅描述对数组而不是组成它们的元素进行操作。例如,在某些允许的语言中,当A和B是数组或矩阵时,编写C = A + B。在低级语言中,您可以通过调用BLAS或Eigen库函数来描述向量化编程风格,而不是手动编写循环。这个问题的其他答案关注于向量化的那种含义和更高级的语言。


25
这个和循环展开有什么不同? - Jeremy Powell
3
编译器在自动向量化展开的循环时是否更容易实现呢? - Nikos Athanasiou
2
@StephenCanon 如何检查某些行是否已被矢量化?如果使用 objdump,输出中应寻找什么? - user1823664
5
向量化是编译器可以为你完成的一项任务,但也是程序员自己明确执行的任务。操作系统不涉及其中。 - Stephen Canon
1
@user1823664 在 objdump 中应该存在 SIMD 指令和寄存器。这是矢量化加法汇编的一个示例 - rcode
显示剩余6条评论

51

向量化是将标量程序转换为向量程序的术语。向量化程序可以从单个指令中运行多个操作,而标量程序只能一次对一对操作数进行操作。

来源于 维基百科:

标量方法:

for (i = 0; i < 1024; i++)
{
   C[i] = A[i]*B[i];
}

矢量化方法:

for (i = 0; i < 1024; i+=4)
{
   C[i:i+3] = A[i:i+3]*B[i:i+3];
}

3
本质上不是和标量方法一样吗?你的语法和循环进展不同,但在底层仍然要乘以4次。但不知何故它可能会更快,可能是因为CPU有一些称为向量化的技巧指令。 - mskw
3
看起来我将在这里回答自己的问题。矢量化方法中的语法,当编译器看到它时,会将其转换为优化的 CPU 指令,从而实现向量乘法。类似于 SIMD。 - mskw
1
@mskw: 这是伪代码,不是实际的 C 向量扩展语法。在真正手动向量化的代码中,它看起来像这样:“__m128 va = _mm_loadu_ps( A+i )”,以此类推,并使用存储内置函数“_mm_mul_ps( va, vb );”。如果要使用 AVX2 来执行比静态编译器不容易自动向量化的更复杂的操作,请参见如何使用SIMD计算字符出现次数的更长示例。 - Peter Cordes

22

向量化 在科学计算中被广泛使用,其中需要高效处理大块数据。

在实际的编程应用中,我知道它在 NUMPY 中使用(不确定其他情况)。

Numpy (Python 科学计算包) 使用 向量化 来快速操作 n 维数组,如果使用内置 Python 处理数组的选项来做,通常会更慢。

虽然有很多解释,但以下是 NUMPY DOCUMENTATION PAGE 中定义的 向量化:

向量化描述了代码中没有任何明确的循环、索引等,当然这些事情仍然在优化的预编译 C 代码中发生。向量化的代码具有许多优点,其中包括:

  1. 向量化的代码更简洁,更易读

  2. 更少的代码行通常意味着更少的错误

  3. 代码更接近标准数学表示法(通常更容易正确编写数学结构)

  4. 向量化会产生更“Pythonic”的代码。没有向量化,我们的代码将会充斥着低效和难以阅读的 for 循环。


15

简单来说,向量化是指优化算法,使它可以利用处理器中的SIMD指令。

AVX、AVX2和AVX512是(英特尔)执行同一操作的指令集,可以在一个指令中对多个数据进行操作。例如,AVX512意味着您可以一次操作16个整数值(4字节)。这意味着,如果您有一个包含16个整数的向量,并且想将每个整数的值加倍然后再加上10,您可以将这些值加载到通用寄存器[a、b、c]中16次并执行相同的操作,也可以将所有16个值加载到SIMD寄存器[xmm、ymm]中并一次性执行操作,从而加速向量数据的计算。

我们通过重新设计数据,利用向量化来优化程序的运行。

唯一的问题是如何处理条件语句,因为条件语句会分支执行流程。这可以通过屏蔽处理来解决,即将条件建模为算术操作。例如,如果我们想将值加10,如果它大于100,我们可以这样做。

if(x[i] > 100) x[i] += 10; // this will branch execution flow.

或者我们可以将条件建模为算术运算,创建一个条件向量c,

c[i] = x[i] > 100; // storing the condition on masking vector
x[i] = x[i] + (c[i] & 10) // using mask

虽然这只是一个非常简单的例子...因此,c是我们用来执行基于它的值的二进制运算的掩码向量。这避免了执行流程的分支并启用了矢量化。

矢量化和并行化一样重要。因此,我们应该尽可能多地利用它。所有现代处理器都具有用于重型计算工作负载的SIMD指令集。我们可以通过矢量化优化我们的代码以使用这些SIMD指令,这类似于将我们的代码并行化以在现代处理器上运行多个内核。

我想提到OpenMP,它使用编译指示让您通过矢量化代码。我认为这是一个很好的起点。同样,也可以说OpenACC。


14

矢量化算术是指在单个步骤中对数字列表或“向量”执行单个数学运算的能力。由于科学计算与超级计算有关,而矢量化算术最初出现在超级计算机上,因此我们经常在Fortran中看到它的身影。如今,几乎所有台式机CPU都通过技术(如Intel的SSE)提供某种形式的矢量化算术。GPU也提供了一种形式的矢量化算术。


10

我认为,由英特尔的人员提出的思想很容易理解。

矢量化是将算法从逐个处理单个值转变为同时处理一组值的过程。现代CPU提供直接支持向量运算的功能,即一条指令应用于多个数据(SIMD)。

例如,一个具有512位寄存器的CPU可以容纳16个32位单精度double值并执行单个计算。

比逐个执行指令快16倍。结合线程和多核CPU,将带来数量级的性能提升。

链接:https://software.intel.com/en-us/articles/vectorization-a-key-tool-to-improve-performance-on-modern-cpus

在Java中,从JDK 15(2020年)开始,已添加了此选项,或者在JDK 16(2021年)后添加。请参阅官方问题


2

希望你一切都好!

向量化指的是将标量实现转换为向量实现的所有技术,其中单个操作同时处理多个实体,而不是一次处理一个实体。

向量化是一种优化代码以高效处理大量数据的技术。在NumPy、pandas等科学应用中可以看到向量化的应用,同时在Matlab、图像处理、自然语言处理等领域也可以使用这种技术。总体来说,它优化了程序的运行时间和内存分配。

希望你已经得到了答案!

谢谢。


如果您在高级语言(如Python)中对数组的单个元素执行标量循环操作,则您的代码不是矢量化的。 矢量化代码是替代方案,其中仅在优化函数内部迭代元素,而不在源代码中可见。 我假设您已经知道这一点,但是在该句子中插入“标量”编码的定义使其听起来像是在谈论编译器将标量循环转换为矢量代码。 - Peter Cordes
C/C++编译器可以自动向量化,但不会除了有时候的memcpy之外发明调用库函数。 - Peter Cordes
谢谢您添加评论,但我想尽可能简单地表达矢量化是指将标量实现转换为矢量实现的所有技术,其中单个操作同时处理多个实体,而不是一次处理一个实体。 - KaranKulshrestha
没错,那是正确的。我建议你修改你的回答,实际上说出来,而不是听起来像你在说“对数组的单个元素执行操作”会神奇地变成使用SIMD、线程和/或本机代码进行优化操作(对于那些不已经编译为本机代码的语言)。 - Peter Cordes

0

我会将向量化定义为给定语言的一个特性,其中如何迭代某个集合的元素的责任可以从程序员(例如显式循环元素)委托给语言提供的某个方法(例如隐式循环)。

那么,我们为什么要这样做呢?

  1. 代码可读性。对于某些情况(但不是全部情况),一次操作整个集合而不是其元素比操作每个程序员指定的循环操作更容易阅读和编码;
  2. 一些解释型语言(例如R、Python、Matlab..但不包括Julia)在处理显式循环时速度非常慢。在这些情况下,向量化使用底层编译指令进行这些“元素顺序处理”,可以比处理每个程序员指定的循环操作快几个数量级;
  3. 大多数现代CPU(以及现在的GPU)都具有内置并行化功能,当我们使用语言提供的向量化方法而不是自己实现的元素操作顺序时,可以利用这种功能;
  4. 类似地,我们选择的编程语言可能会针对某些向量化操作(例如矩阵操作)使用软件库(例如BLAS/LAPACK),这些库利用了CPU的多线程能力,另一种形式的并行计算。
请注意,对于第3点和第4点,一些语言(尤其是Julia)允许使用程序员定义的顺序处理(例如for循环)来利用这些硬件并行化,但当使用语言提供的向量化方法时,这会自动且在后台发生。
现在,虽然向量化具有许多优点,但有时候使用显式循环比向量化更直观地表达算法(在这种情况下,我们可能需要使用复杂的线性代数运算、单位矩阵和对角矩阵等来保持我们的“向量化”方法),如果使用显式排序形式没有计算上的劣势,那么应该选择这种方法。

-6

请参考上面的两个答案。我想要补充一点,进行向量化的原因是这些操作可以轻松地由超级计算机和多处理器并行执行,从而获得很大的性能提升。在单处理器计算机上将不会获得性能提升。


17
“在单处理器计算机上不会有性能提升”的说法是不正确的。现代大多数处理器都具有(有限的)向量化硬件支持(如SSE、Altivec等,由stephentyrone命名),在使用时可以显著提高速度。 - sleske
1
谢谢,我忘记了并行化也可以在那个层面上完成。 - Larry Watanabe

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接