gpt4 book ai didi

c - 如何使用英特尔内在函数从 256 个 vector 中提取 8 个整数?

转载 作者:太空狗 更新时间:2023-10-29 15:05:18 26 4
gpt4 key购买 nike

我正在尝试通过使用 256 位 vector (英特尔内在函数 - 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 位)的 vector 也需要 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] ... )。所以我必须再次将因子提取为单独的整数。
    我想知道是否有任何可能的方法来做到这一点?

    最佳答案

    智能编译器可以获得 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]

    但是要回答您提出的问题:

    当您有这么多独立的添加发生时,延迟并不是什么大问题。 4个标量的吞吐量 add Haswell 上每个时钟的指令更相关。

    k1fac2等已经在连续内存中,那么使用 SIMD 可能是值得的。否则所有将它们移入/移出 vector 寄存器的改组和数据传输绝对不值得。 (即东西编译器发出来实现 _mm256_set_epi32 (0,kfac6,kfac5,kfac4,kfac3,kfac2,kfac1,kfac)

    通过使用 AVX2 收集表加载,您可以避免需要将索引重新放入整数寄存器。但是在 Haswell 上收集速度很慢,所以可能不值得。也许在布罗德韦尔值得。

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

    如果您确实需要从 __m256i 中提取 8 个 32 位整数到整数寄存器 ,您有三种主要的策略选择:
  • vector 存储到 tmp 数组并加载标量
  • ALU shuffle 指令,如 pextrd ( _mm_extract_epi32 )。使用 _mm256_extracti128_si256将高车道进入单独的 __m128i .
  • 两种策略的混合(例如,将高位 128 存储到内存中,同时在低位使用 ALU 内容)。

  • 根据周围的代码,这三个中的任何一个在 Haswell 上都可能是最佳的。
    pextrd r32, xmm, imm8在 Haswell 上是 2 uops,其中一个需要端口 5 上的 shuffle 单元。这是很多 shuffle uops,因此纯 ALU 策略只有在您的代码在 L1d 缓存吞吐量方面遇到瓶颈时才会有用。 (与内存带宽不同)。 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] ,或进入 __m256i 之间的 union 和一个 int大批。您仍然可以存储到 int数组而不是 __m256i成员在保持数组对齐的同时避免 union 类型双关,但最简单的方法是使用 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 周期 vector 存储/整数重新加载延迟比使用 ALU 操作获得所有结果所需的时间要长,但如果存在大量指令级并行性并且 ALU 吞吐量出现瓶颈,存储/重新加载仍然可能很好而不是延迟。

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

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

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

    关于c - 如何使用英特尔内在函数从 256 个 vector 中提取 8 个整数?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/44847767/

    26 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com