本文主要是介绍SIMD指令集介绍,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
# 介绍
本学期,我们将在多项作业中使用 SIMD(单指令多数据)指令。这些是在称为向量的宽寄存器上运行的指令集。对于我们的作业,这些向量通常为 256 位宽,尽管您可能偶尔使用 128 位版本。通常,作用于这些宽寄存器的指令会将其视为值数组。然后,他们将对数组中的每个值独立执行操作。在硬件中,这可以通过多个并行工作的ALU来实现。因此,尽管这些指令执行的算术比“正常”指令多很多倍,但它们可以与正常指令一样快。
通常,我们将使用“内部函数”访问这些指令。这些函数通常直接对应于特定的汇编指令。这将使我们能够编写始终如一地访问此特殊功能的 C 代码,而不会失去拥有 C 编译器的所有好处。
# 内联参考
我们将使用的内在功能是英特尔定义的接口。因此,英特尔的文档(可在此处找到)是这些功能的综合参考。请注意,本文档包含与实验室计算机上不支持的指令相对应的函数。为避免看到这些,请务必仅选中侧面标有“AVX”、“AVX2”和“SSE”到“SSE4.2”的框。
英特尔的参考资料通常描述了伪代码中的指令,这些指令使用诸如
```
a[63:0] := b[127:64]
```
表示将向量 B 的位 64 到 127(含)分配给向量 A 的位 0 到 63。
# 头文件
若要使用内部函数,需要包含相应的头文件。对于内联函数,我们将使用它是:
```
#include <smmintrin.h>
#include <immintrin.h>
```
# 用 C 语言表示向量
为了表示可能存储在 C 寄存器之一中的 256 位值,我们将使用以下类型之一:
* __m256 (8个float)
* __m256d (4个double)
* __m256i (n个int)
由于其中每个都只是一个 256 位值,因此,如果要使用的函数需要“错误”类型的值,则可以在这些类型之间进行转换。例如,您可能希望使用旨在加载浮点值以加载整数的函数。在内部,期望这些类型的函数只是操作寄存器或内存中的 256 位值。
## 类型和内部函数的 128 位版本
还有 128 位矢量类型和相应的指令。要使用它,在大多数情况下,您可以在类型名称中替换为 _mm256_ 和 _mm_ __m128 在类型名称中替换为 __m256 。
在某些情况下,仅存在 256 位版本的指令。
## 设置和提取值
如果要加载 128 位值的常量,则需要使用内部函数之一。最容易的是,您可以使用名称以 开头 _mm_setr 的函数之一。例如:
```
__m256i values = _mm256_setr_epi32(0x1234, 0x2345, 0x3456, 0x4567, 0x5678, 0x6789, 0x789A, 0x89AB);
```
make 包含 values 8 个 32 位整数, , , 0x3456 , 0x4567 0x1234 0x2345 0x5678 , , , , 0x6789 0x789A . 0x89AB 然后,我们可以通过执行以下操作来提取这些整数中的每一个:
```
int first_value = _mm256_extract_epi32(values, 0);
// first_value == 0x1234
int second_value = _mm256_extract_epi32(values, 1);
// second_value == 0x2345
```
请注意,只能将常量索引传递给 和类似函数的 _mm256_extract_epi32 第二个参数。
## 加载和存储值
要从内存加载值数组或将值数组存储到内存中,我们可以使用以 或 _mm256_storeu 开头 _mm256_loadu 的内联函数:
```
int arrayA[8];
_mm256_storeu_si256((__m128i*) arrayA, values);
// arrayA[0] == 0x1234
// arrayA[1] == 0x2345
// ...
int arrayB[8] = {10, 20, 30, 40, 50, 60, 70, 80};
values = _mm256_loadu_si256((__m128i*) arrayB);
// 10 == arrayB[0] == _mm256_extract_epi32(values, 0)
// 20 == arrayB[1] == _mm256_extract_epi32(values, 1)
// ...
```
## 算术
要实际对值执行算术运算,每个支持的数学运算都有函数。例如:
```
__m256i first_values = _mm256_setr_epi32(10, 20, 30, 40);
__m256i second_values = _mm256_setr_epi32( 5, 6, 7, 8);
__m256i result_values = _mm256_add_epi32(first_values, second_values);
// _mm_extract_epi32(result_values, 0) == 15
// _mm_extract_epi32(result_values, 1) == 26
// _mm_extract_epi32(result_values, 2) == 37
// _mm_extract_epi32(result_values, 3) == 48
```
## 向量中不同类型的值
这些示例将 256 位值视为 8 个 32 位整数的数组。有一些指令处理许多不同类型的值,包括其他大小的整数或浮点数。您通常可以通过函数名称中指示值类型的存在来判断需要哪种类型。例如,“epi32”表示 an __m256 中的“8 个 32 位值”或 ( __m128 名称代表“扩展打包整数,32 位”)。您将在名称中看到其他一些约定:
* si256 – 有符号 256 位整数
* si128 – 有符号 128 位整数
* epi8 , , epi32 — epi64 有符号 8 位整数(A 中的 32 个和 A __m256 __m128 中的 16 个)或有符号 32 位整数或有符号 64 位整数的向量
* epu8 — 无符号 8 位整数的 vecotr(当操作对有符号和无符号数字的操作之间存在差异时,例如转换为更大的整数或乘法)
* epu16 , epu32 — 无符号 16 位整数或 8 个无符号 32 位整数数组(当操作与有符号不同时)
* ps — “打包单” — 8 个单精度浮子
* pd — “打包双倍” — 4 双倍
* ss — 一个浮点数(仅使用 256 位或 128 位值的 32 位)
* sd — 一个双精度值(仅使用 256 位或 256 位值的 64 位)
# 示例(在 C 中)
以下两个 C 函数是等效的
```
int add_no_AVX(int size, int *first_array, int *second_array) {
for (int i = 0; i < size; ++i) {
first_array[i] += second_array[i];
}
}
int add_AVX(int size, int *first_array, int *second_array) {
int i = 0;
for (; i + 8 <= size; i += 8) {
// load 256-bit chunks of each array
__m256i first_values = _mm_loadu_si256((__m256i*) &first_array[i]);
__m256i second_values = _mm_loadu_si256((__m256i*) &second_array[i]);
// add each pair of 32-bit integers in the 256-bit chunks
first_values = _mm256_add_epi32(first_values, second_values);
// store 256-bit chunk to first array
_mm_storeu_si256((__m256i*) &first_array[i], first_values);
}
// handle left-over
for (; i < size; ++i) {
first_array[i] += second_array[i];
}
}
```
# 精选的方便的内在函数:
## 算术
* _mm256_add_epi32(a, b) — 将其 __m256i 参数视为 8 个 32 位整数。如果 a 包含 32 位整数 a0, a1, a2, a3, a4, a5, a6, a7 并 b 包含 b0, b1, b2, b3, b4, b5, b6, b7 ,则返回 a0 + b0, a1 + b1, a2 + b2, a3 + b3, a4 + b4, a5 + b5, a6 + a6, a7 + b7 。(与 vpaddd 指令相对应。
* _mm256_add_epi16(a, b) — 与 _mm256_add_epi32 16 位整数相同,但使用 16 位整数。如果 a 包含 16 位整数 a0, a1, ..., a15 并 b 包含 b1, b2, ..., b15 ,则返回 a0 + b0, a1 + b1, ..., a15 + b15 。(与 vpaddw 指令相对应。
* _mm256_add_epi8(a, b) — 与 _mm256_add_epi32 8 位整数相同,但使用 8 位整数。
* _mm256_mullo_epi16(x, y) :将 x 和 y 视为 16 位有符号整数的向量,将每对整数相乘,并将结果截断为 16 位。
* _mm256_mulhi_epi16(x, y) :将 x 和 y 视为 16 位有符号整数的向量,将每对整数相乘得到一个 32 位整数,然后返回每个 32 位整数结果的前 16 位。
* _mm256_srli_epi16(x, N) :处理 x 和 16 位有符号整数的向量,并返回逻辑上将每个右移的结果 N 。(还有 epi32 32 位或 64 位整数的 and epi64 变体。
* _mm256_slli_epi16(x, N) :处理 x 和 16 位有符号整数的向量,并返回将每个向左移动的结果 N 。(还有 epi32 32 位或 64 位整数的 and epi64 变体。
* _mm256_hadd_epi16(a, b) — (“horizontal add”) 将其 __m128i 参数视为 16 位整数的向量。如果 a contains 和 b contains b0, b1, b2, b3, ..., b15 a0, a1, a2, a3, ..., a15 ,则返回 a0 + a1, a2 + a3, a4 + a5, a6 + a7, b0 + b1, b2 + b3, b4 + b5, b6 + b7, a8 + a9, a10 + a11, a12 + a13, a14 + a15, b8 + b9, b10 + b11, b12 + b13, b14 + b15 。请注意,这通常比 _mm_add_epi16 慢得多。(与 vphaddw 指令相对应。
## 加载/存储
* _mm256_loadu_si256 , _mm256_storeu_si256 — 向内存加载或存储 256 位或从内存加载或存储 256 位。请注意,您可以使用 _mm256_storeu_si256 存储到临时数组中,如下所示:
```
unsigned short values_as_array[16];
__m256i values_as_vector;
_mm256_storeu_si128((__m256i*) &values_as_array[0], values_as_vector);
```
* _mm_loadu_si128 , _mm_storeu_si128 — 将 128 位加载或存储到内存或从内存加载或存储。(对应于 vmovdqu 说明。它们的工作方式与 完全相同, _mm256_loadu_si256 只是它们使用 type __m128i 而不是 __m256i .
* 要存储向量中的 64 位或 32 位,一种方法是使用提取操作和 memcpy:
```
unsigned short first_four_values_as_array[4];
__m256i values_as_vector;
*(long*)(&first_four_values_as_array[0]) = _mm256_extract_epi64(values_as_vetor, 0);
```
(此代码实际上不是标准投诉;它违反了“严格别名”规则。但是在 SIMD 分配的 Makefile 中,我们使用 compiler 选项 -fno-strict-aliasing 禁用了它。不违反严格别名规则的替代方法是使用联合,而不是将指针转换为 int* or to use memcpy ,这通常针对小副本进行了优化。
* _mm_cvtsi32_si128 :将 32 位加载到 128 位向量中:
```
unsigned short values[2];
__m128i values_as_vector; // only using first 32 bits = 2 shorts
values_as_vector = _mm_cvtsi32_si128( *(int*) &values[0]);
```
(此代码实际上不是标准投诉;它违反了“严格别名”规则。但是在 SIMD 分配的 Makefile 中,我们使用 compiler 选项 -fno-strict-aliasing 禁用了它。不违反严格别名规则的替代方法是使用联合,而不是将指针转换为 int* .)
* _mm_cvtsi32_si128 :将 64 位加载到 128 位向量中:
```
unsigned short values[4];
__m128i values_as_vector; // only using first 64 bits = 4 shorts
values_as_vector = _mm_cvtsi64_si128( *(long*) &values[0]);
```
(此代码实际上不是标准投诉;请参阅上面的评论) _mm_cvtsi32_si128
* 要在 256 位向量中加载 32 位或 64 位,可以使用 _mm_cvtsi32_si128 或 _mm_cvtsi32_si256 一起使用 _mm266_zextsi128_si256 将 128 位向量转换为 256 位向量。
* _mm256_maskstore_epi32(int *addr, __m256i mask, __m256i a) — 存储 a at addr 的 32 位值,但仅存储 mask 指定的 32 位值。如果设置了每个 32 位整数 mask 的最高有效位(即符号),则存储值。例如:
```
int values[8] = { 0xF, 0xF, 0xF, 0xF, 0xF, 0xF, 0xF, 0xF };
__m256i a = __m256_setr_epi32(1,2,3,4,5,6,7,8);
__m256i mask = __m256_setr_epi32(0,-1,0,0,-1,0,-1,-1);
_mm256_maskstore_epi32(&values[0], mask, a);
```
应导致包含以下内容的值
```
{ 0xF, 2, 0xF, 0xF, 5, 0xF, 7, 8 }
```
* 有关详细信息,请参阅英特尔的参考资料,在“加载”和“存储”类别下
## 设置常量
* _mm256_setr_epi32 — 返回一个 __m256i 包含指定 32 位整数的值。第一个整数参数将位于写入内存时地址最低的部分 __m256i 。例如:
```
__m256i value1 = _mm256_setr_epi16(0, 1, 2, 3, 4, 5, 6, 7);
```
产生 value1 与 in value2 相同的结果
```
int array[8] = {0, 1, 2, 3, 4, 5, 6, 7};
__m256i value2 = _mm256_loadu_si256((__m256i*) &array[0]);
```
* _mm_setr_epi32 — 返回一个 __m128i 包含指定 32 位整数的值。第一个整数参数将位于写入内存时地址最低的部分 __m128i 。例如:
```
__m128i value1 = _mm_setr_epi32(0, 1, 2, 3);
```
产生 value1 与 in value2 相同的结果
```
int array[4] = {0, 1, 2, 3, 4, 5, 6, 7};
__m128i value2 = _mm_loadu_si128((__m256i*) &array[0]);
```
* _mm256_setr_epi16 — 与 _mm256_setr_epi32 16 位整数相同,但使用 16 位整数。例如:
```
__m256i value1 = _mm256_setr_epi16(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15);
```
产生 value1 与 in value2 相同的结果
```
short array[8] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
__m256i value2 = _mm256_loadu_si256((__m256i*) &array[0]);
```
* _mm256_setr_epi8 , — 与 _mm256_setr_epi32 和 _mm_setr_epi32 相同, _mm_setr_epi8 但具有 8 位整数。
* _mm_set1_epi32 , , — 返回一个 __m128i 值,表示适当大小的值数组, _mm_set1_epi16 _mm_set1_epi8 其中数组的每个元素都具有相同的值。例如:
```
__m128i value = _mm_set1_epi16(42);
```
具有与以下相同的效果:
```
__m128i value = _mm_setr_epi16(42, 42, 42, 42, 42, 42, 42, 42);
```
* _mm256_set_epi8 , etc. — 与 _mm256_setr_epi8 等相同,但其参数的顺序相反
* 有关更多信息,请参阅英特尔的参考资料,在“设置”类别下
## 提取部分值
* _mm256_extract_epi32(a, index) 从 256 位向量中提取 index 'th 32 位整数 a 。索引为 0 的整数是将存储在最低内存地址的整数,如果 a 复制到内存中。 index 必须是一个常量。
例如
```
__m256i a = _mm256_setr_epi32(0, 10, 20, 30, 40, 50, 60, 70);
int x = _mm256_extract_epi32(a, 2);
```
20 分配给 x 。
* _mm_extract_epi32(a, index) 从 128 位向量中提取 index 'th 32 位整数 a 。 index 必须是常量。
* _mm256_extract_epi16(a, index) 与 _mm256_extract_epi32 16 位整数相同,但具有 16 位整数
* _mm256_extracti128_si256(a, index) 从 256 位向量中提取 index 128 位向量 a 。 index 必须是常量。
例如
```
__m256i a = _mm256_setr_epi32(0, 10, 20, 30, 40, 50, 60, 70);
__m128i result = _mm256_extracti128_si256(a, 1);
```
相当于
```
__m128i result = _mm_setr_epi32(40, 50, 60, 70);
```
* 有关更多信息,请参阅英特尔的参考资料,搜索“提取”或在“Swizzle”和“Cast”类别下查找。
## 在值类型之间转换
* _mm256_cvtepu8_epi16(eight_bit_numbers) :采用包含 16 个 8 位数字的 128 位向量,并将其转换为包含 16 个 16 位有符号整数的 256 位向量。例如:
```
__m128i value1 = _mm_setr_epi8(10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150);
__m256i value2 = _mm256_cvtepu8_epi16(value1);
```
导致 value2 包含与我们执行的相同的值:
```
__m256i value2 = _mm256_setr_epi16(10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150);
```
* []()_mm256_packus_epi16(a, b) 获取 256 位向量中的 16 位有符号整数 a , b 并将它们转换为 8 位无符号整数的 256 位向量。结果包含 的前 a 8 个整数,后跟 的前 8 个整数,后跟 的最后 8 个整数 b a ,后跟 的最后 8 个整数 b 。超出范围的值设置为 255 或 0。
例如:
```
__m256i a = _mm256_setr_epi16(10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160);
__m256i b = _mm256_setr_epi16(170, 180, 190, 200, 210, 220, 230, 240, 250, 260, 270, 25, 15, 5, -5, -15);
__m256i result = _mm256_packus_epi16(a, b)
```
设置 result 与我们所做的相同:
```
__m256i result = _mm256_setr_epu8(
10, 20, 30, 40, 50, 60, 70, 80, /* first 8 integers from a */
170, 180, 190, 200, 210, 220, 230, 240, /* first eight integers from b */
90, 100, 110, 120, 130, 140, 150, /* last 8 integers from a */
250, 255, 255, 25, 15, 5, 0, 0, /* last 8 integers from b */
/* 260, 270 became 255; -5, -15 became 0 */
);
```
* _mm256_zextsi128_si256(a) 采用 128 位向量 a ,并通过添加 0 将其转换为 256 位向量。
* 有关更多信息,请参阅英特尔在“Swizzle”和“Move”和“Cast”类别下的参考。
## 重新排列 256 位值
* _mm256_permute2x128_si256(a, b, mask) 采用两个 256 位向量, a 并 b 根据 mask 将这些向量的 128 位半部分组合成一个新的 256 位向量。 mask 是一个单字节整数常量。最低有效半字节指定放置在结果向量的最低地址中的值,最高有效半字节指定放置在结果向量的最高地址中的值。
每个掩码半字节选择的值为:
* 0 选择前 128 位 a
* 1 选择第二个 128 位 a
* 2 选择前 128 位 b
* 3 选择第二个 128 位 b
* 4 到 15 选择常量 0 (忽略 a 和 b 的值)
例如,要重复 a 的第二个 128 位,可以提供如下示例所示 0x11 的掩码:
```
__m256i a = _mm256_setr_epi32(0, 1, 2, 3, 4, 5, 6, 7);
__m256i b = _mm256_setr_epi32(8, 9, 10, 11, 12, 13, 14, 15);
__m256i result = _mm256_permute2x128_si256(a, b, 0x11);
// result == _mm256_setr_epi32(4, 5, 6, 7, 4, 5, 6, 7)
```
要生成前 128 位后跟后跟 1 位后跟 128 位 a b 的结果,将提供如下 0x30 掩码:
```
__m256i a = _mm256_setr_epi32(0, 1, 2, 3, 4, 5, 6, 7);
__m256i b = _mm256_setr_epi32(8, 9, 10, 11, 12, 13, 14, 15);
__m256i result = _mm256_permute2x128_si256(a, b, 0x30);
// result == _mm256_setr_epi32(0, 1, 2, 3, 12, 13, 14, 15)
```
* _mm256_unpackhi_epi16(a, b) 将 16 位整数与 256 位向量中每个 128 位半部分的上四分之一交错, a 然后 b .例如:
```
__m256i a = _mm256_setr_epi16(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15);
__m256i b = _mm256_setr_epi16(16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31);
__m256i result = _mm256_unpackhi_epi16(a, b);
```
与
```
__m256i result = _mm256_setr_epi16(
/* top quarter of first half of a and b */
4, 20, 5, 21, 6, 22, 7, 23,
/* top quarter of second half of a and b */
12, 28, 13, 29, 14, 30, 15, 31
)
```
* _mm256_unpacklo_epi16(a, b) 就像, _mm256_unpackhi_epi16 但它从 和 的每半部分 a b 的底部四分之一取 16 位整数
* _mm256_permutevar8x32_epi32(x, indexes) — 通过为向量中的每个 32 位索引生成一个 32 位值的向量 indexes ,从向量中检索该索引处的 32 位值 x 并将其放入结果中。例如:
```
__m256i x = _mm256_setr_epi32(10, 20, 30, 40, 50, 60, 70, 80)
__m256i indexes = _mm256_setr_epi32(3, 3, 0, 1, 2, 3, 6, 7);
__m256i result = _mm256_permutevar8x32_epi32(x, indexes)
```
等同于:
```
__m256i reuslt = _mm256_setr_epi32(40, 40, 10, 20, 30, 70, 80);
```
* 有关更多信息,请参阅英特尔在“Swizzle”和“Move”以及“Cast”和“Shift”类别下的参考。
## 重新排列 128 位值
* _mm_unpackhi_epi16(a, b) 将 128 位向量上半部分的 16 位整数交错, a 然后 b .例如:
```
__m128i a = _mm_setr_epi16(0, 1, 2, 3, 4, 5, 6, 7);
__m128i b = _mm_setr_epi16(8, 9, 10, 11, 12, 13, 14, 15);
__m256i result = _mm_unpackhi_epi16(a, b);
```
与
```
__m128i result = _mm_setr_epi16(
4, 20, 5, 21, 6, 22, 7, 23,
)
```
* _mm_shuffle_epi8(a, mask) 重新排列 a 根据 的 mask 字节并返回结果。 mask 是 8 位整数 (type __m128i ) 的向量,指示如何重新排列每个字节:
* 如果掩码中的字节设置了高位(大于 127),则输出的相应字节为 0;
* 否则,输入中指定的字节号将复制到输出的相应字节。字节使用 0 进行编号,以表示如果将向量复制到内存中,将存储在最低地址中的字节。
例如:
```
__m128i value1 = _mm_setr_epi8(10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160);
__m128i mask = _mm_setr_epi8(0x80, 0x80, 0x80, 5, 4, 3, 0x80, 7, 6, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80);
__m128i value2 = _mm_shuffle_epi8(value1, mask);
```
应产生与以下结果相同的结果:
```
__m128i value2 = _mm_setr_epi8(0, 0, 0, 60, 50, 40, 0, 80, 70, 0, 0, 0, 0, 0, 0, 0, 0);
/* e.g. since 3rd element of mask is 5, 3rd element of output is 60, element 5 of the input */
```
* 有关更多信息,请参阅英特尔在“Swizzle”和“Move”以及“Cast”和“Shift”类别下的参考。
# 示例(组装指令)
指令
```
paddd %xmm0, %xmm1
```
接收两个 128 位值,一个在寄存器中,另一个在寄存器 %xmm0 %xmm1 中。这些寄存器中的每一个都被视为两个 64 位值的数组。将每对 64 位值相加,并将结果存储在 %xmm1 中。
例如,如果 %xmm0 包含 128 位值(以十六进制写入):
```
0x0000 0000 0000 0001 FFFF FFFF FFFF FFFF
```
并 %xmm1 包含 128 位值(以十六进制写入):
```
0xFFFF FFFF FFFF FFFE 0000 0000 0000 0003
```
然后 %xmm0 ,将被视为包含数字和 (或 0xFFFFFFFFFFFFFFFF ),并 %xmm1 被视为包含数字 -2 1 和 -1 3 。 paddd 将添加 1 和 -2 to produce -1 and -1 and 3 to produce 2, so the final value of %xmm1' 将是:
```
0xFFFF FFFF FFFF FFFF 0000 0000 0000 0002
```
如果我们将此值解释为两个 64 位整数的数组,则为 -1 和 2 。
这篇关于SIMD指令集介绍的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!