【问题标题】:How to extract 8 integers from a 256 vector using intel intrinsics?如何使用 intel 内在函数从 256 向量中提取 8 个整数?
【发布时间】:2017-12-04 11:47:00
【问题描述】:

我正在尝试通过使用 256 位向量(英特尔内在函数 - AVX)来提高我的代码的性能。

我有一个支持 SSE1 到 SSE4.2 和 AVX/AVX2 扩展的 I7 Gen.4(Haswell 架构)处理器。

这是我正在尝试增强的代码 sn-p:

/* 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) ////////////////////

但问题是这些因素将被用作表索引( table[kfac] ... )。所以我必须再次将因子提取为单独的整数。 不知道有没有办法呢??

【问题讨论】:

  • 当你有这么多独立的添加发生时,延迟并不是什么大问题。每个时钟 4 条标量 add 指令的吞吐量更为相关。如果k1fac2 等已经在连续内存中,那么使用 SIMD 可能是值得的。否则,所有将它们输入/输出向量 regs 的洗牌和数据传输绝对不值得。 (并且 AVX2 收集在 Haswell 上很慢,否则您可以将其用于表加载。)

标签: c x86 simd intrinsics avx


【解决方案1】:

智能编译器可以将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,这绝对不值得。 (即编译器发出的东西来实现_mm256_set_epi32 (0,kfac6,kfac5,kfac4,kfac3,kfac2,kfac1,kfac)。

您可以通过使用 AVX2 收集表加载来避免将索引返回到整数寄存器中。但是在 Haswell 上收集速度很慢,所以可能不值得。在 Broadwell 上也许值得。

在 Skylake 上,gather 速度很快,因此如果您可以对 LUT 结果进行任何操作都可以进行 SIMD 处理,那将是一件好事。如果您需要将所有收集结果提取回单独的整数寄存器,则可能不值得。


如果您确实需要将 8x 32 位整数从 __m256i 提取到整数寄存器中,您有以下三种主要策略选择:

  • 向量存储到 tmp 数组和标量加载
  • ALU shuffle 指令,如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) 使其明确并提醒自己可以更有效地访问底部元素。

存储/重新加载具有良好的吞吐量。包括 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-data 准备好后,即使是第一个结果也不会在 6 个周期内准备好。

混合策略为您提供两全其美的优势:ALU 提取前 2 或 3 个元素可以在任何使用它们的代码上开始执行,隐藏存储/重新加载的存储转发延迟。

 _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 个元素是最佳选择,在这种情况下您只需要存储/重新加载上面的通道。使用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 操作获得所有结果的延迟要长,但是如果存在大量指令级并行性并且 ALU 吞吐量遇到瓶颈,存储/重新加载仍然可能很好而不是延迟。

对于更多更小的元素(8 位或 16 位),存储/重新加载绝对有吸引力。用 ALU 指令提取前 2 到 4 个元素还是不错的。甚至可能是vmovd r32, xmm,然后用整数移位/掩码指令将其分开是很好的。


您对矢量版本的周期计数也是虚假的。三个_mm256_add_epi32 操作是独立的,Haswell 可以并行运行两个vpaddd 指令。 (Skylake 可以在一个周期内运行所有三个,每个周期都有 1 个周期延迟。)

超标量流水线乱序执行意味着延迟和吞吐量之间存在很大差异,并且跟踪依赖链非常重要。有关更多优化指南,请参阅 http://agner.org/optimize/ 和 标签 wiki 中的其他链接。

【讨论】:

  • 非常好的和有启发性的答案......我会尽力按照你说的去做,看看结果。
  • @A.nechi:我添加了一个回答实际问题的部分,以防人们通过查看问题标题或搜索到达这里。就您而言,我认为仅用于计算索引是不值得的。
猜你喜欢
  • 1970-01-01
  • 2011-11-22
  • 1970-01-01
  • 1970-01-01
  • 2023-04-10
  • 2015-02-03
  • 1970-01-01
  • 2019-05-31
  • 2022-10-29
相关资源
最近更新 更多