前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >[强基固本-视频压缩]第十二章:向量指令 第一部分

[强基固本-视频压缩]第十二章:向量指令 第一部分

作者头像
用户1324186
发布2024-03-20 14:26:17
1090
发布2024-03-20 14:26:17
举报
文章被收录于专栏:媒矿工厂

题目:Vector Instructions. Part I 链接:https://www.elecard.com/page/article_vector_instructions_part1 作者:Dmitry Farafonov 内容整理:王妍 本文是对 ELECARD Video Compression Book 第十二章的翻译。本章节介绍了一些基本的向量指令,并展示了几个采用它们的算法和函数。

引言

向量计算是在执行单个处理器指令时,对多个数据块同时执行相同类型的多个操作。这一原理也被称为 SIMD(单指令多数据)。这个名字源于与向量代数的明显相似性:向量之间的操作具有单一符号表示,但涉及对向量各分量执行多个算术操作。

最初,向量计算是由专用协处理器执行的,这些协处理器曾是超级计算机的主要组成部分。在 1990 年代,一些 x86 CPU 和其他架构的处理器配备了向量扩展,这些扩展是特殊的大型寄存器,以及用于操作它们的向量指令。

在需要执行多种相同类型的操作并实现高性能计算的地方,会使用向量指令,例如在计算数学、数学建模、计算机图形学和计算机游戏等各种应用中。没有向量计算,现在无法实现视频信号处理所需的计算性能,尤其是视频编码和解码。需要注意的是,在某些应用和算法中,向量指令并不能提高性能。

本文展示了使用向量指令的示例,并实现了几个采用它们的算法和函数。这些示例主要来自图像和信号处理领域,但对其他应用领域的软件开发者也可能有用。向量指令有助于提高性能,但并非总能成功:要充分发挥计算机的潜力,开发者不仅需要小心精确,而且往往还需要有创造力。

指令和寄存器

向量计算是在执行单个处理器指令时同时执行多种相同类型的操作。这一原理不仅在专用处理器中实现,也在 x86 和 ARM CPU 中以向量扩展的形式实现,这些向量扩展是特殊的向量寄存器,比通用寄存器更宽。为了使用这些寄存器,提供了特殊的向量指令,扩展了处理器的指令集。

图 1:标量和向量计算

通常,向量指令执行与标量(或常规)指令相同的操作(见图1),但由于它们处理的数据量更大,因此性能更高。当执行指令时,通用寄存器预期持有一个特定类型的单个数据项(例如,某个长度的整数或浮点数),而向量寄存器同时持有该寄存器能够容纳的相同类型的多个独立数据项。当执行向量指令时,可以同时对这些数据执行相同数量的独立操作,计算性能因此得到相应的提升。通过同时执行多个相同的操作来提高处理器性能是向量扩展的主要目的。

在 x86 CPU 中,第一个向量扩展是使用八个 64 位寄存器(MM0–MM7)的 MMX 指令集。MMX 让位于更强大的 128 位 SSE 浮点和 SSE2 整数及双精度浮点指令,这些指令使用 XMM0–XMM7 寄存器。后来,128 位的 SSE3、SSSE3、SSE4.1 和 SSE4.2 指令集相继推出,它们扩展了 SSE 和 SSE2,并增加了几个有用的指令。上述集合中的大多数指令使用两个寄存器操作数;结果被写入其中一个寄存器,而其原始内容将丢失。

向量扩展发展的下一个里程碑是更强大的 256 位 AVX 和 AVX2 指令,它们使用 256 位的 YMM0–YMM15 寄存器。值得注意的是,这些指令使用三个寄存器操作数:两个寄存器存储源数据,第三个寄存器接收操作结果,而其他两个寄存器的内容保持不变。最新的向量指令集是 AVX-512,它使用 32 个 512 位寄存器(ZMM0–ZMM31)。AVX-512 在一些服务器 CPU 中用于高性能计算。

随着 64 位 CPU 的普及,MMX 指令集变得过时。然而,随着 AVX 和 AVX2 的出现,SSE 和 SSE2 指令并没有被废弃,仍然被积极使用。x86 CPU 保持向后兼容性:如果 CPU 支持 AVX2,它也支持 SSE/SSE2、SSE3、SSSE3、SSE4.1 和 SSE4.2。同样,如果 CPU 支持 SSSE3,例如,它也支持所有早期的指令集。

对于 ARM CPU,开发了 NEON 向量扩展。这些 64 位和 128 位的向量指令使用 32 个 64 位寄存器或 16 个 128 位寄存器(ARM64 有 32 个 128 位寄存器)。

由于向量指令与特定的处理器架构(甚至往往是特定处理器)绑定,使用这些指令的程序变得不可移植。因此,为了实现可移植性,需要使用不同指令集实现相同算法的多个版本。

内嵌函数(Intrinsics)

开发者如何使用向量指令?首先,它们可以在汇编代码中使用。

同样,开发者可以在不使用内联汇编代码的情况下,从使用高级语言编写的程序中访问向量指令,包括 C/C++。为此,使用了所谓的内嵌函数,这些是嵌入编译器的对象。在头文件中声明一个或多个数据类型,并且这些类型的变量对应于一个向量寄存器。(从编程的角度来看,这是一种特殊类型的固定长度数组,不允许访问单个数组元素。)头文件还声明了接受上述类型参数并返回值的函数,它们在编程层面上执行与相应向量操作相同的操作。实际上,这些函数并不是用软件实现的:相反,编译器在生成目标代码时,用向量指令替换了对它们的每个调用。因此,内嵌函数允许使用高级语言编写程序,其性能接近或等同于汇编程序。

使用内嵌函数所需的一切就是包含相应的头文件,并且在使用某些编译器时,应启用相应的编译器选项。尽管它们不是 C/C++ 语言标准的一部分,但主流编译器如 GCC、Clang、MSVC、Intel 都支持内嵌函数。

它们还有助于简化各种数据类型的处理。请注意,至少在 x86 CPU 架构方面,处理器无法访问寄存器中存储的数据类型。当执行向量指令时,其数据被解释为与该指令相关联的特定类型,例如浮点数或特定大小的整数(有符号或无符号)。开发者需要确保计算的有效性,这需要相当小心,特别是当数据类型有时发生变化时:例如,在整数乘法中,乘积的大小等于乘数大小的总和。内嵌函数可以在一定程度上简化这个任务。

因此,XMM 向量寄存器(SSE)有三个关联的数据类型:__m128,一个包含四个单精度浮点数的“数组” __m128d,一个包含两个双精度浮点数的“数组” __m128i,一个 128 位寄存器,可以被视为 8-、16-、32- 或 64 位数字的“数组”。由于特定的向量指令通常只与三种数据类型之一(单精度浮点数、双精度浮点数或整数)一起工作,表示向量指令的函数参数也具有上述三种类型之一。AVX2 类型系统具有类似的设计:它提供了 __m256(浮点数)、__m256d(双精度浮点数)和__m256i(整数)类型。

NEON 内嵌函数实现了一个更先进的类型系统,其中 128 位寄存器与 int32x4_t、int16x8_t、int8x16_t、float32x4_t 和 float64x2_t 类型相关联。NEON 还提供了多寄存器数据类型,如 int8x16x2_t。在这种系统中,寄存器内容的特定类型和大小始终已知,因此在类型转换和数据大小变化时出错的可能性较小。

考虑一个使用 SSE2 指令集实现的简单函数示例。

代码语言:javascript
复制
// 1.2.1: SSE2 内嵌函数示例
// 对于 int32_t
#include <stdint.h>
// 对于 SSE2 内嵌函数
#include <emmintrin.h>

void bar(void)
{
    int32_t array_a[4] = {0,2,1,2}; // 128位
    int32_t array_b[4] = {8,5,0,6};
    int32_t array_c[4];

    __m128i a, b, c;
    a = _mm_loadu_si128((__m128i*)array_a); // 将array_a加载到寄存器a
    b = _mm_loadu_si128((__m128i*)array_b);

    c = _mm_add_epi32(a, b); // 必须是{ 8,7,1,8 }
    _mm_storeu_si128((__m128i*)array_c, c);
}

在这个示例中,array_a 的内容被加载到一个向量寄存器中,array_b 的内容被加载到另一个寄存器中。然后,相应的 32 位寄存器元素被相加,结果被写入第三个寄存器,最后复制到 array_c。这个示例突出了内嵌函数的另一个显著特点。虽然_mm_add_epi32 接受两个寄存器参数并返回一个寄存器值,但与_mm_add_epi32 对应的 padd 指令只有两个实际的寄存器操作数,其中一个接收操作结果,因此丢失了其原始内容。为了在编译“c = _mm_add_epi32(a, b)”表达式时保留寄存器内容,编译器添加了在寄存器之间复制数据的操作。

内嵌函数的名称选择是为了提高源代码的可读性。在 x86 架构中,名称由三部分组成:前缀、操作指定和标量数据类型后缀(图2,а)。前缀指示向量寄存器的大小:_mm_表示 128 位,_mm256_表示 256 位,_mm512_表示 512 位。一些数据类型指定如表 1 所示。ARM 中的 NEON 内嵌函数具有类似的命名模式(图2,b)。请记住,有两种类型的向量寄存器(64 位和 128 位)。字母 q 表示指令适用于 128 位寄存器。

图 2:SSE2(a)和 ARM NEON(b)中的内嵌函数名称

表 1:x86 内嵌函数的数据类型指定

内嵌函数数据类型的名称(如__m128i 和其他)和函数已经成为不同编译器中的事实上的标准。在本文的剩余部分中,我们将使用内嵌函数名称而不是助记符代码来指代向量指令。

基本向量指令

本节描述了基本的指令类别。它列出了经常使用且有帮助的指令示例——主要来自 x86 架构,但也包括ARM NEON。

与RAM的数据交换

在处理器能够处理存储在 RAM 中的数据之前,首先必须将数据加载到处理器寄存器中。然后,在处理完成后,需要将数据写回 RAM。

大多数向量指令是寄存器到寄存器的——也就是说,它们的操作数是向量寄存器,结果被写入相同的寄存器。有一系列专门的指令用于与 RAM 进行数据交换。

_mm_loadu_si128(__m128i* addr)指令从 RAM 中检索以 addr 为起始地址的 128 位长连续整数数组,并将其写入选定的向量寄存器。相比之下,_mm_storeu_si128(__m128i* addr, __m128i a)指令从寄存器 a 中复制 128 位长的连续数据数组到 RAM,从 addr 地址开始。这些指令使用的地址 addr 可以是任意的(但当然,不应在读写时引起数组溢出)。_mm_load_si128 和_mm_store_si128 指令与上述指令类似,可能更高效,但要求 addr 是 16 字节的倍数(换句话说,对齐到 16 字节边界);否则,在执行时会抛出硬件异常。

存在专门的指令用于读取和写入单精度和双精度浮点数据(128 位长),即_mm_loadu_ps/_mm_storeu_ps和_mm_loadu_pd/_mm_storeu_pd。

经常需要加载比向量寄存器能容纳的更少的数据。为此,《mm_loadl_epi64(__m128i* addr)指令从 RAM 中检索以 addr 为起始地址的连续 64 位数组,并将其写入选定向量寄存器的最低有效半部分,将最高有效半部分的位设置为零。_mm_storel_epi64(__m128i* addr, __m128i a)指令具有相反的效果,从 addr 地址开始将寄存器的最低有效 64 位复制到 RAM 中。_mm_cvtsi32_si128(int32_t a)指令将一个 32 位整数变量复制到向量寄存器的最低有效 32 位,其余部分设置为零。_mm_cvtsi128_si32(__m128i a)指令则相反,将寄存器的最低有效 32 位复制到一个整数变量中。

逻辑和比较操作

SSE2 指令集提供了执行以下逻辑操作的指令:AND、OR、XOR 和 NAND。相应的指令命名为_mm_and_si128、_mm_or_si128、_mm_xor_si128 和_mm_andnot_si128。这些指令与相应的整数位操作完全类似,不同之处在于数据大小为 128 位而不是 32 或 64 位。

经常使用的_mm_setzero_si128()指令将目标寄存器的所有位设置为零,是通过使用 XOR 操作实现的,其中两个操作数相同。

逻辑指令与比较指令密切相关。这些指令比较两个源寄存器的相应元素,并检查是否满足特定条件(相等或不等)。如果满足条件,目标寄存器元素的所有位都设置为1;否则,设置为 0。例如,_mm_cmpeq_epi32(__m128i a, __m128i b)指令检查寄存器 a 和 b 的 32 位元素是否彼此相等。可以使用逻辑指令组合几个不同条件检查的结果。

算术和移位操作

这组指令无疑是最常用的。

对于浮点计算,x86 和 ARM 都有实现单精度和双精度数的所有四种算术操作和平方根计算的指令。x86 架构对单精度数有以下指令:_mm_add_ps、_mm_sub_ps、_mm_mul_ps、_mm_div_ps 和_mm_srqt_ps。

让我们考虑一个简单的浮点算术操作示例。就像第 2 节(1.2.1)中的示例一样,这里对两个数组 src0 和 src1 的元素进行求和,并将结果写入数组 dst。要求和的元素数量在参数 len 中指定。如果 len 不是向量寄存器容纳的元素数量的倍数(在这种情况下是四个和两个),则剩余的元素将通过传统方式处理,不进行向量化。

代码语言:javascript
复制
// 1.3.1 两个数组元素的和
/* SS E和 SSE2所需 */
void sum_float(float src0[], float src1[], float dst[], size_t len)
__m128 x0, x1; // 浮点数,单精度
size_t len4 = len & ~0x03;
for (size_t i = 0; i < len4; i += 4)
    x0 = _mm_loadu_ps(src0 + i); // 加载四个浮点值
    x1 = _mm_loadu_ps(src1 + i);
    x0 = _mm_add_ps(x0, x1);
    _mm_storeu_ps(dst + i, x0);
for (size_t i = len4; i < len; i++)
    dst[i] = src0[i] + src1[i];
}
void sum_double(double src0[], double src1[], double dst[], size_t len)
__m128d x0, x1; // 浮点数,双精度
size_t len2 = len & ~0x01;
for (size_t i = 0; i < len2; i += 2)
    x0 = _mm_loadu_pd(src0 + i); // 加载两个双精度值
    x1 = _mm_loadu_pd(src1 + i);
    x0 = _mm_add_pd(x0, x1);
    _mm_storeu_pd(dst + i, x0);
if (len2 != len)
    dst[len2] = src0[len2] + src1[len2];

对于特定的整数算术操作,通常有几种相同类型的指令,每种指令都针对特定大小的数据。考虑加法和减法。对于 16 位有符号整数,《mm_add_epi16 指令执行加法,_mm_sub_epi16 指令执行减法。类似的指令也适用于 8 位、32 位和 64 位整数。对于 16 位数据,左逻辑移位和右逻辑移位分别由_mm_slli_epi16 和_mm_srli_epi16 实现。然而,只有 16 位和 32 位数据大小实现了右算术移位:这个操作由_mm_srai_epi16 和_mm_srai_epi32 指令执行。ARM NEON 也为这些操作提供了指令,涵盖了 8 位、16 位、32 位和 64 位数据大小,包括有符号和无符号。

_mm_slli_si128(__m128i a, int imm)和_mm_srli_si128(__m128i a, int imm)指令将寄存器内容视为一个 128 位的数字,并分别向左和向右移动 imm 字节(不是位!)。

图 3:水平加法

SSE3 和 SSSE3 指令集引入了水平加法的指令(图3):_mm_hadd_ps(__m128 a, __m128 b)、_mm_hadd_pd(__m128d a, __m128d b)、_mm_hadd_epi16(__m128i a, __m128i b)和_mm_hadd_epi32(__m128i a, __m128i b)。通过水平加法,同一寄存器中的相邻元素会被相加。同样提供了水平减法指令(如_mm_hsub_ps 等),以相同的方式减去数字。在 ARM NEON 指令集中也存在实现成对加法的类似指令(例如 vpaddq_s16(int16x8_t a, int16x8_t b))。

一般来说,在整数乘法中,乘积的位深度等于乘数位深度的总和。因此,一个寄存器中的 16 位元素与另一个寄存器中的相应元素相乘,在一般情况下,会产生 32 位的乘积,这将需要两个寄存器而不是一个来容纳。

_mm_mullo_epi16(__m128i a, __m128i b)指令将寄存器 a 和 b 中的 16 位元素相乘,将 32 位乘积的最低有效 16 位写入目标寄存器。其对应的指令_mm_mulhi_epi16(__m128i a, __m128i b)将乘积的最高有效 16 位写入目标寄存器。这些指令产生的结果可以使用我们将在下面讨论的_mm_unpacklo_epi16 和_mm_unpackhi_epi16 指令组合成 32 位乘积。当然,如果乘数足够小,单独使用_mm_mullo_epi16 就足够了。

图 4:_mm_madd_epi16 指令

_mm_madd_epi16(__m128i a, __m128i b)指令将寄存器 a 和 b 中的 16 位元素相乘,然后将产生的相邻 32 位乘积相加(图 4)。这条指令在实现各种滤波器、离散余弦变换和其他需要大量组合乘法和加法的变换中特别有用:它立即将乘积转换为方便的 32 位格式,并减少了所需的加法数量。

ARM NEON 有相当多样化的乘法指令集。例如,它提供了增加乘积大小的指令(如 vmull_s16),也有不增加乘积大小的指令,还有将向量与标量相乘的指令(如 vmul_n_f32)。NEON 中没有类似于_mm_madd_epi16 的指令;相反,提供了根据公式执行乘加指令。x86 架构中也存在这样的指令(FMA 指令集),但仅适用于浮点数。

至于整数向量除法,在 x86 或 ARM 上都没有实现。

排列和交错

下面讨论的处理器指令类型没有标量对应物。当它们被执行时,不会产生新值。相反,数据在寄存器内部进行排列,或者来自多个源寄存器的数据以特定顺序写入目标寄存器。这些指令乍一看似乎不是很有用,但实际上非常重要。许多算法没有它们就无法高效实现。

图 5:按掩码复制

多个 x86 和 ARM 向量指令实现了按掩码复制(图 5)。考虑有一个源数组、一个目标数组和一个与目标大小相同的索引数组,索引数组中的每个元素对应于目标数组的一个元素。索引数组元素的值指向要复制到相应目标数组元素的源数组元素。通过指定不同的索引,可以实现各种元素排列和复制。

向量指令使用向量寄存器或它们的组合作为源和目标数组。索引数组可以是一个向量寄存器或一个整数常量,其位组对应于目标寄存器元素,并编码源寄存器元素。

实现按掩码复制的指令之一是 SSE2 的_mm_shuffle_epi32(__m128i a, const int im)指令,它将源寄存器中选定的 32 位元素复制到目标寄存器。索引数组是第二个操作数,是一个整数常量,指定复制掩码。这条指令通常与标准宏_MM_SHUFFLE 一起使用,提供了一种更直观的方式来指定复制掩码。例如,执行

a = _mm_shuffle_epi32(b,_MM_SHUFFLE(0,1,2,3)); b 的 32 位元素以相反的顺序写入a寄存器。而执行

a = _mm_shuffle_epi32(b,_MM_SHUFFLE(2,2,2,2)); b 寄存器的第三个元素的相同值被写入 a 寄存器的所有元素。

_mm_shufflelo_epi16 和_mm_shufflehi_epi16 指令以类似的方式工作,但分别从寄存器的最低有效和最高有效半部分复制选定的 16 位元素,并将另一半以原样写入目标寄存器。作为一个例子,我们将展示如何仅用三个操作使用这些指令与_mm_shuffle_epi32 一起,将 128 位寄存器的 16 位元素按相反顺序排列。这是如何做到的:

代码语言:javascript
复制
// a: a0 a1 a2 a3 a4 a5 a6 a7
a = _mm_shuffle_epi32(a, _MM_SHUFFLE(1,0,3,2));  // a4 a5 a6 a7 a0 a1 a2 a3
a = _mm_shufflelo_epi16(a, _MM_SHUFFLE(0,1,2,3)); // a7 a6 a5 a4 a0 a1 a2 a3
a = _mm_shufflehi_epi16(a, _MM_SHUFFLE(0,1,2,3)); // a7 a6 a5 a4 a3 a2 a1 a0

首先,寄存器的最高有效和最低有效半部分被交换,然后每个半部分的 16 位元素按相反顺序排列。

SSSE3 集合中的_mm_shuffle_epi8(__m128i a, __m128i i)指令也按掩码复制,但按字节操作。(然而,这条指令使用相同的寄存器作为源和目标,所以它更像是“按掩码排列”。)索引由第二个寄存器操作数中的字节值指定。这条指令允许比前面讨论的指令更多样化的排列,使得在许多情况下简化和加速计算成为可能。因此,上述整个示例可以用一个指令重新实现:a = _mm_shuffle_epi8(a, i);为此,i 寄存器的字节应具有以下值(从最低有效字节开始):4,15,12,13,10,11,8,9,6,7,4,5,2,3,0,1

在 ARM NEON 中,按掩码复制使用几个指令实现,这些指令与一个源寄存器(例如,vtbl1_s8 (int8x8_t a, int8x8_t idx))或一组寄存器(例如,vtbl4_u8(uint8x8x4_t a, uint8x8_t idx))一起工作。vqtbl1q_u8(uint8x16_t t, uint8x16_t idx)指令类似于_mm_shuffle_epi8。

图 6:洗牌

使用向量指令实现的另一种操作是交错。考虑以下数组:元素,元素 ... 元素。当这些数组被洗牌时,它们的元素以以下顺序组合成一个新的数组:(图6)。相应的向量指令也使用寄存器——只有两个——作为源数组。显然,由于这个操作不会改变数据的大小,所以也应该有两个目标寄存器。

x86 上的向量指令只能有一个目标寄存器,因此洗牌指令只处理输入数据的一半。因此,_mm_unpacklo_epi16(__m128i a, __m128i b)将 a 和 b 寄存器最低有效半部分的 16 位元素洗牌,而其_mm_unpackhi_epi16(__m128i a, __m128i b)的对应指令则对最高有效半部分执行相同的操作。8 位、32 位和 64 位指令的工作方式类似。_mm_unpacklo_epi64 和_mm_unpackhi_epi64 指令本质上是将两个寄存器的最低有效和最高有效 64 位结合起来。通常一起使用成对的指令。

ARM NEON 中存在类似的指令(VZIP 指令系列)。其中一些使用两个目标寄存器而不是一个,因此处理输入数据的全部。还有一些反向工作的指令(VUZP),x86 上没有对应的指令。

_mm_alignr_epi8(__m128i a, _m128i b, int imm)指令从选定的字节 imm 开始,将源寄存器b的字节复制到目标寄存器,并从最低有效字节开始,从寄存器 a 复制其余部分。假设 a 寄存器的字节具有值 a0..a15,b 寄存器的字节具有值 b0..b15。那么,执行 a = _mm_alignr_epi8(a, b, 5); 以下字节将被写入 a 寄存器:b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15, a0, a1, a2, a3, a4。ARM NEON 提供了这种类型的指令,它们使用特定大小的元素而不是字节。

AVX 和 AVX2 指令

x86 向量指令的进一步发展标志着 256 位 AVX 和 AVX2 指令的出现。这些指令为开发者提供了什么?

首先,不再是八个(或十六个)128 位的 XMM 寄存器,而是有十六个 256 位的寄存器 YMM0–YMM15,其中最低有效 128 位是 XMM 向量寄存器。与 SSE 不同,这些指令接受三个而不是两个寄存器操作数:两个源寄存器和一个目标寄存器。执行指令后,源寄存器的内容不会丢失。

几乎所有在早期 SSE–SSE4.2 指令集中实现的操作都在 AVX/AVX2 中存在,最重要的是算术操作。有完全类似于_mm_add_epi32、_mm_madd_epi16、_mm_sub_ps、_mm_slli_epi16 等的指令,但它们的工作速度是原来的两倍。

AVX/AVX2 没有_mm_loadl_epi64 和_mm_cvtsi32_si128(以及相应的输出指令)的确切等效物。相反,引入了_mm256_maskload_epi32 和_mm256_maskload_epi64 指令,它们使用位掩码从RAM加载所需数量的 32 位和 64 位值。

还添加了新的指令,如_mm256_gather_epi32、_mm256_gather_epi64 及其浮点等效指令,它们使用起始地址和块偏移量以块的形式加载数据,而不是连续数组。当所需数据在 RAM 中不是连续存储,并且需要许多操作来检索和组合它时,这尤其有用。

AVX2 有数据交错和排列指令,如_mm256_shuffle_epi32 和_mm256_alignr_epi8。它们具有独特的属性,使它们与 AVX/AVX2 指令的其他部分区分开来。例如,常规的算术指令将 YMM 寄存器视为 256 位数组。相比之下,这些指令将 YMM 视为两个 128 位寄存器,并以与相应 SSE 指令完全相同的方式对它们执行操作。

考虑一个具有以下 32 位元素的寄存器:A0, A1, A2, A3, A4, A5, A6, A7。然后,在执行 a = _mm256_shuffle_epi32(a, _M_SHUFFLE(0,1,2,3)); 寄存器内容将变为 A3, A2, A1, A0, A7, A6, A5, A4。

其他指令,如_mm256_unpacklo_epi16、_mm256_shuffle_epi8 和_mm256_alignr_epi8,以类似的方式工作。

AVX2 还添加了新的排列和交错指令。例如,_mm256_permute4x64_epi64(__m256i, int imm)以类似于_mm_shuffle_epi32 排列 32 位元素的方式排列 64 位寄存器元素。

我在哪里可以获得有关向量指令的信息?

首先,访问微处理器供应商的官方网站。英特尔有一个在线参考,您可以在那里找到所有指令集内嵌函数的全面描述。ARM CPU 也有类似的参考。

如果您想了解向量指令的实际用途,请参考音频和视频编解码器的免费软件实现。像 FFmpeg、VP9 和 OpenHEVC 这样的项目使用向量指令,这些项目的源代码提供了它们的使用示例。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-03-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 媒矿工厂 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 引言
  • 指令和寄存器
  • 内嵌函数(Intrinsics)
  • 基本向量指令
    • 与RAM的数据交换
      • 逻辑和比较操作
        • 算术和移位操作
          • 排列和交错
            • AVX 和 AVX2 指令
            • 我在哪里可以获得有关向量指令的信息?
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档