首页 文章

如何使用intel内在函数从256个向量中提取8个整数?

提问于
浏览
5

我正在尝试使用256位向量(Intel intrinsics - 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) ////////////////////

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

1 回答

  • 3

    智能编译器可以将 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可能是值得的 . 否则,所有的混洗和数据传输都会使它们进入/退出向量寄存器,这使得它绝对不值得 . (即东西编译器发出以实现 _mm256_set_epi32 (0,kfac6,kfac5,kfac4,kfac3,kfac2,kfac1,kfac) .

    您可以通过对表加载使用AVX2集合来避免将索引返回到整数寄存器 . 但哈斯韦尔的聚会很慢,所以可能不值得 . 也许在Broadwell上值得 .

    在Skylake上,聚合速度很快,所以如果你能用你的LUT结果做任何事情就可以了 . 如果需要将所有收集结果提取回单独的整数寄存器,则可能不值得 .


    If you did need to extract 8x 32-bit integers from a __m256i into integer registers ,您有三种主要的策略选择:

    • 向量存储到tmp数组和标量加载

    • ALU随机播放指令,如 pextrd_mm_extract_epi32 ) . 使用 _mm256_extracti128_si256 将高通道变为单独的 __m128i .

    • 两种策略的混合(例如,在低半部分使用ALU内容时将高128存储到内存中) .

    根据周围的代码,这三个中的任何一个都可能是Haswell的最佳选择 .

    pextrd r32, xmm, imm8 在Haswell上是2 uops,其中一个需要在port5上使用shuffle单元 . 这是一个很多的随机微博,所以如果你的代码在L1d缓存吞吐量上存在瓶颈,那么纯ALU策略将会很好 . (与内存带宽不同) . movd r32, xmm 只有1个uop,并且编译器确实知道在编译 _mm_extract_epi32(vec, 0) 时使用它,但你也可以编写 int foo = _mm_cvtsi128_si32(vec) 使其明确并提醒自己可以更有效地访问底部元素 .

    存储/重新加载具有良好的吞吐量 . 包括Haswell在内的Intel SnB系列CPU每个时钟可以运行两个负载,IIRC存储转发可以从一个对齐的32字节存储器工作到它的任何4字节元素 . 但要确保它是一个对齐的商店,例如进入 _Alignas(32) int tmp[8] ,或进入 __m256iint 数组之间的并集 . 您仍然可以存储到 int 数组而不是 __m256i 成员,以避免在仍然使数组对齐时出现联合类型 - 但是最简单的方法是使用C 11 alignas 或C11 _Alignas .

    _Alignas(32) int tmp[8];
     _mm256_store_si256((__m256i*)tmp, vec);
     ...
     foo2 = tmp[2];
    

    但是,存储/重新加载的问题是延迟 . 在存储数据准备好后,即使第一个结果也不会准备好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 然后用整数选择那个shift / mask指令很好 .


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

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

相关问题