如何使用英特尔内在函数从 256 向量中提取 8 个整数?
How to extract 8 integers from a 256 vector using intel intrinsics?
我正在尝试使用 256 位向量(英特尔内在函数 - AVX)来提高我的代码的性能。
我有一个支持 SSE1 到 SSE4.2 和 AVX/AVX2 扩展的 I7 Gen.4(Haswell 架构)处理器。
这是我要增强的代码片段:
/* code snipet */
kfac1 = kfac + factor; /* 7 cycles for 7 additions */
kfac2 = kfac1 + factor;
kfac3 = kfac2 + factor;
kfac4 = kfac3 + factor;
kfac5 = kfac4 + factor;
kfac6 = kfac5 + factor;
kfac7 = kfac6 + factor;
k1fac1 = k1fac + factor1; /* 7 cycles for 7 additions */
k1fac2 = k1fac1 + factor1;
k1fac3 = k1fac2 + factor1;
k1fac4 = k1fac3 + factor1;
k1fac5 = k1fac4 + factor1;
k1fac6 = k1fac5 + factor1;
k1fac7 = k1fac6 + factor1;
k2fac1 = k2fac + factor2; /* 7 cycles for 7 additions */
k2fac2 = k2fac1 + factor2;
k2fac3 = k2fac2 + factor2;
k2fac4 = k2fac3 + factor2;
k2fac5 = k2fac4 + factor2;
k2fac6 = k2fac5 + factor2;
k2fac7 = k2fac6 + factor2;
/* code snipet */
我从英特尔手册中找到了这个。
整数加法 ADD 需要 1 个周期(延迟)。
8 个整数(32 位)的向量也需要 1 个周期。
所以我尝试过这样做:
fac = _mm256_set1_epi32 (factor )
fac1 = _mm256_set1_epi32 (factor1)
fac2 = _mm256_set1_epi32 (factor2)
v1 = _mm256_set_epi32 (0,kfac6,kfac5,kfac4,kfac3,kfac2,kfac1,kfac)
v2 = _mm256_set_epi32 (0,k1fac6,k1fac5,k1fac4,k1fac3,k1fac2,k1fac1,k1fac)
v3 = _mm256_set_epi32 (0,k2fac6,k2fac5,k2fac4,k2fac3,k2fac2,k2fac1,k2fac)
res1 = _mm256_add_epi32 (v1,fac) ////////////////////
res2 = _mm256_add_epi32 (v2,fa1) // just 3 cycles //
res3 = _mm256_add_epi32 (v3,fa2) ////////////////////
但问题是这些因素将被用作 tables 索引 ( table[kfac] ... )。所以我必须再次将因子提取为单独的整数。
请问有什么办法可以做到吗??
聪明的编译器可以将 table+factor
放入寄存器并使用索引寻址模式将 table+factor+k1fac6
作为地址。检查 asm,如果编译器没有为您执行此操作,请尝试更改源代码以手持编译器:
const int *tf = table + factor;
const int *tf2 = table + factor2; // could be lea rdx, [rax+rcx*4] or something.
...
foo = tf[kfac2];
bar = tf2[k2fac6]; // could be mov r12, [rdx + rdi*4]
但要回答您提出的问题:
当您有那么多独立的添加发生时,延迟并不是什么大问题。 Haswell 上每个时钟 4 个标量 add
指令的吞吐量更为相关。
如果 k1fac2
等已经在连续内存中,那么使用 SIMD 可能是值得的。否则,为获得向量 regs in/out 而进行的所有改组和数据传输绝对不值得。 (即编译器发出的东西来实现 _mm256_set_epi32 (0,kfac6,kfac5,kfac4,kfac3,kfac2,kfac1,kfac)
.
通过对 table 负载使用 AVX2 收集,您可以避免需要将索引返回到整数寄存器中。但是在 Haswell 上收集速度很慢,所以可能不值得。也许在布罗德韦尔值得。
在 Skylake 上,收集速度很快,因此如果您可以对 LUT 结果进行任何操作,都可以进行 SIMD 处理,那就太好了。如果您需要将所有收集结果提取回单独的整数寄存器,这可能不值得。
如果您确实需要从 __m256i
中提取 8 个 32 位整数到整数寄存器,您有三种主要的策略选择:
- 矢量存储到 tmp 数组和标量加载
- ALU 洗牌指令,如
pextrd
(_mm_extract_epi32
)。使用 _mm256_extracti128_si256
将高车道变成单独的 __m128i
.
- 两种策略的混合(例如,将高位 128 存储到内存中,同时在低位使用 ALU 内容)。
根据周围的代码,这三个中的任何一个都可能是 Haswell 上的最佳选择。
pextrd r32, xmm, imm8
在 Haswell 上是 2 微指令,其中之一需要端口 5 上的洗牌单元。这是很多 shuffle uops,所以纯 ALU 策略只有在您的代码在 L1d 缓存吞吐量上遇到瓶颈时才会好用。 (与内存带宽不同)。 movd r32, xmm
只有 1 uop,编译器知道在编译 _mm_extract_epi32(vec, 0)
时使用它,但你也可以写 int foo = _mm_cvtsi128_si32(vec)
来明确它并提醒自己底部元素可以被更多地访问高效。
Store/reload 具有良好的吞吐量。包括 Haswell 在内的英特尔 SnB 系列 CPU 每个时钟可以 运行 两次加载,并且 IIRC 存储转发从对齐的 32 字节存储到它的任何 4 字节元素。但要确保它是一家对齐的商店,例如进入 _Alignas(32) int tmp[8]
,或进入 __m256i
和 int
数组之间的联合。您仍然可以存储到 int
数组而不是 __m256i
成员中以避免联合类型双关,同时仍然使数组对齐,但最简单的方法是使用 C++11 alignas
或C11_Alignas
.
_Alignas(32) int tmp[8];
_mm256_store_si256((__m256i*)tmp, vec);
...
foo2 = tmp[2];
但是,store/reload 的问题是延迟。在存储数据准备好之后,即使是第一个结果也不会准备好 6 个周期。
混合策略为您提供两全其美的方法:提取前 2 或 3 个元素的 ALU 让执行开始于使用它们的任何代码,隐藏 store/reload 的存储转发延迟。
_Alignas(32) int tmp[8];
_mm256_store_si256((__m256i*)tmp, vec);
__m128i lo = _mm256_castsi256_si128(vec); // This is free, no instructions
int foo0 = _mm_cvtsi128_si32(lo);
int foo1 = _mm_extract_epi32(lo, 1);
foo2 = tmp[2];
// rest of foo3..foo7 also loaded from tmp[]
// Then use foo0..foo7
你可能会发现最好用 pextrd
做前 4 个元素,在这种情况下你只需要 store/reload 上路。使用 vextracti128 [mem], ymm, 1
:
_Alignas(16) int tmp[4];
_mm_store_si128((__m128i*)tmp, _mm256_extracti128_si256(vec, 1));
// movd / pextrd for foo0..foo3
int foo4 = tmp[0];
...
由于较大的元素(例如 64 位整数)较少,纯 ALU 策略更具吸引力。 6 周期向量存储/整数重新加载延迟比使用 ALU 操作获得所有结果所需的时间更长,但如果有很多指令级并行性并且你遇到瓶颈,store/reload 仍然可以ALU 吞吐量而不是延迟。
更多更小的元素(8 位或 16 位),store/reload 绝对有吸引力。用 ALU 指令提取前 2 到 4 个元素还是不错的。甚至 vmovd r32, xmm
然后用整数 shift/mask 指令将其分开是好的。
你的矢量版循环计数也是假的。三个_mm256_add_epi32
操作是独立的,Haswell可以运行两个vpaddd
指令并行。 (Skylake 可以 运行 在一个周期内完成所有三个,每个都有 1 个周期延迟。)
超标量流水线乱序执行意味着延迟和吞吐量之间存在很大差异,跟踪依赖链非常重要。有关更多优化指南,请参阅 http://agner.org/optimize/, and other links in the x86 标签 wiki。
我正在尝试使用 256 位向量(英特尔内在函数 - AVX)来提高我的代码的性能。
我有一个支持 SSE1 到 SSE4.2 和 AVX/AVX2 扩展的 I7 Gen.4(Haswell 架构)处理器。
这是我要增强的代码片段:
/* code snipet */
kfac1 = kfac + factor; /* 7 cycles for 7 additions */
kfac2 = kfac1 + factor;
kfac3 = kfac2 + factor;
kfac4 = kfac3 + factor;
kfac5 = kfac4 + factor;
kfac6 = kfac5 + factor;
kfac7 = kfac6 + factor;
k1fac1 = k1fac + factor1; /* 7 cycles for 7 additions */
k1fac2 = k1fac1 + factor1;
k1fac3 = k1fac2 + factor1;
k1fac4 = k1fac3 + factor1;
k1fac5 = k1fac4 + factor1;
k1fac6 = k1fac5 + factor1;
k1fac7 = k1fac6 + factor1;
k2fac1 = k2fac + factor2; /* 7 cycles for 7 additions */
k2fac2 = k2fac1 + factor2;
k2fac3 = k2fac2 + factor2;
k2fac4 = k2fac3 + factor2;
k2fac5 = k2fac4 + factor2;
k2fac6 = k2fac5 + factor2;
k2fac7 = k2fac6 + factor2;
/* code snipet */
我从英特尔手册中找到了这个。
整数加法 ADD 需要 1 个周期(延迟)。
8 个整数(32 位)的向量也需要 1 个周期。
所以我尝试过这样做:
fac = _mm256_set1_epi32 (factor )
fac1 = _mm256_set1_epi32 (factor1)
fac2 = _mm256_set1_epi32 (factor2)
v1 = _mm256_set_epi32 (0,kfac6,kfac5,kfac4,kfac3,kfac2,kfac1,kfac)
v2 = _mm256_set_epi32 (0,k1fac6,k1fac5,k1fac4,k1fac3,k1fac2,k1fac1,k1fac)
v3 = _mm256_set_epi32 (0,k2fac6,k2fac5,k2fac4,k2fac3,k2fac2,k2fac1,k2fac)
res1 = _mm256_add_epi32 (v1,fac) ////////////////////
res2 = _mm256_add_epi32 (v2,fa1) // just 3 cycles //
res3 = _mm256_add_epi32 (v3,fa2) ////////////////////
但问题是这些因素将被用作 tables 索引 ( table[kfac] ... )。所以我必须再次将因子提取为单独的整数。 请问有什么办法可以做到吗??
聪明的编译器可以将 table+factor
放入寄存器并使用索引寻址模式将 table+factor+k1fac6
作为地址。检查 asm,如果编译器没有为您执行此操作,请尝试更改源代码以手持编译器:
const int *tf = table + factor;
const int *tf2 = table + factor2; // could be lea rdx, [rax+rcx*4] or something.
...
foo = tf[kfac2];
bar = tf2[k2fac6]; // could be mov r12, [rdx + rdi*4]
但要回答您提出的问题:
当您有那么多独立的添加发生时,延迟并不是什么大问题。 Haswell 上每个时钟 4 个标量 add
指令的吞吐量更为相关。
如果 k1fac2
等已经在连续内存中,那么使用 SIMD 可能是值得的。否则,为获得向量 regs in/out 而进行的所有改组和数据传输绝对不值得。 (即编译器发出的东西来实现 _mm256_set_epi32 (0,kfac6,kfac5,kfac4,kfac3,kfac2,kfac1,kfac)
.
通过对 table 负载使用 AVX2 收集,您可以避免需要将索引返回到整数寄存器中。但是在 Haswell 上收集速度很慢,所以可能不值得。也许在布罗德韦尔值得。
在 Skylake 上,收集速度很快,因此如果您可以对 LUT 结果进行任何操作,都可以进行 SIMD 处理,那就太好了。如果您需要将所有收集结果提取回单独的整数寄存器,这可能不值得。
如果您确实需要从 __m256i
中提取 8 个 32 位整数到整数寄存器,您有三种主要的策略选择:
- 矢量存储到 tmp 数组和标量加载
- ALU 洗牌指令,如
pextrd
(_mm_extract_epi32
)。使用_mm256_extracti128_si256
将高车道变成单独的__m128i
. - 两种策略的混合(例如,将高位 128 存储到内存中,同时在低位使用 ALU 内容)。
根据周围的代码,这三个中的任何一个都可能是 Haswell 上的最佳选择。
pextrd r32, xmm, imm8
在 Haswell 上是 2 微指令,其中之一需要端口 5 上的洗牌单元。这是很多 shuffle uops,所以纯 ALU 策略只有在您的代码在 L1d 缓存吞吐量上遇到瓶颈时才会好用。 (与内存带宽不同)。 movd r32, xmm
只有 1 uop,编译器知道在编译 _mm_extract_epi32(vec, 0)
时使用它,但你也可以写 int foo = _mm_cvtsi128_si32(vec)
来明确它并提醒自己底部元素可以被更多地访问高效。
Store/reload 具有良好的吞吐量。包括 Haswell 在内的英特尔 SnB 系列 CPU 每个时钟可以 运行 两次加载,并且 IIRC 存储转发从对齐的 32 字节存储到它的任何 4 字节元素。但要确保它是一家对齐的商店,例如进入 _Alignas(32) int tmp[8]
,或进入 __m256i
和 int
数组之间的联合。您仍然可以存储到 int
数组而不是 __m256i
成员中以避免联合类型双关,同时仍然使数组对齐,但最简单的方法是使用 C++11 alignas
或C11_Alignas
.
_Alignas(32) int tmp[8];
_mm256_store_si256((__m256i*)tmp, vec);
...
foo2 = tmp[2];
但是,store/reload 的问题是延迟。在存储数据准备好之后,即使是第一个结果也不会准备好 6 个周期。
混合策略为您提供两全其美的方法:提取前 2 或 3 个元素的 ALU 让执行开始于使用它们的任何代码,隐藏 store/reload 的存储转发延迟。
_Alignas(32) int tmp[8];
_mm256_store_si256((__m256i*)tmp, vec);
__m128i lo = _mm256_castsi256_si128(vec); // This is free, no instructions
int foo0 = _mm_cvtsi128_si32(lo);
int foo1 = _mm_extract_epi32(lo, 1);
foo2 = tmp[2];
// rest of foo3..foo7 also loaded from tmp[]
// Then use foo0..foo7
你可能会发现最好用 pextrd
做前 4 个元素,在这种情况下你只需要 store/reload 上路。使用 vextracti128 [mem], ymm, 1
:
_Alignas(16) int tmp[4];
_mm_store_si128((__m128i*)tmp, _mm256_extracti128_si256(vec, 1));
// movd / pextrd for foo0..foo3
int foo4 = tmp[0];
...
由于较大的元素(例如 64 位整数)较少,纯 ALU 策略更具吸引力。 6 周期向量存储/整数重新加载延迟比使用 ALU 操作获得所有结果所需的时间更长,但如果有很多指令级并行性并且你遇到瓶颈,store/reload 仍然可以ALU 吞吐量而不是延迟。
更多更小的元素(8 位或 16 位),store/reload 绝对有吸引力。用 ALU 指令提取前 2 到 4 个元素还是不错的。甚至 vmovd r32, xmm
然后用整数 shift/mask 指令将其分开是好的。
你的矢量版循环计数也是假的。三个_mm256_add_epi32
操作是独立的,Haswell可以运行两个vpaddd
指令并行。 (Skylake 可以 运行 在一个周期内完成所有三个,每个都有 1 个周期延迟。)
超标量流水线乱序执行意味着延迟和吞吐量之间存在很大差异,跟踪依赖链非常重要。有关更多优化指南,请参阅 http://agner.org/optimize/, and other links in the x86 标签 wiki。