optimization - 加载和转置八个 8 元素浮点向量

标签 optimization intel intrinsics avx avx2

在运行 DSP 算法的紧密循环之一中,我需要在给定基本数据指针和 AVX2 整数寄存器中的偏移量的情况下加载 8 个 8 元素浮点向量。我当前最快的代码如下所示:

void LoadTransposed(
    const float* data, __m256i offsets,
    __m256& v0, __m256& v1, __m256& v2, __m256& v3, __m256& v4, __m256& v5, __m256& v6, __m256& v7)
{
    const __m128i offsetsLo = _mm256_castsi256_si128(offsets);
    const __m128i offsetsHi = _mm256_extracti128_si256(offsets, 1);
    __m256 a0 = _mm256_loadu_ps(data + (uint32)_mm_cvtsi128_si32(offsetsLo   ));
    __m256 a1 = _mm256_loadu_ps(data + (uint32)_mm_extract_epi32(offsetsLo, 1));
    __m256 a2 = _mm256_loadu_ps(data + (uint32)_mm_extract_epi32(offsetsLo, 2));
    __m256 a3 = _mm256_loadu_ps(data + (uint32)_mm_extract_epi32(offsetsLo, 3));
    __m256 a4 = _mm256_loadu_ps(data + (uint32)_mm_cvtsi128_si32(offsetsHi   ));
    __m256 a5 = _mm256_loadu_ps(data + (uint32)_mm_extract_epi32(offsetsHi, 1));
    __m256 a6 = _mm256_loadu_ps(data + (uint32)_mm_extract_epi32(offsetsHi, 2));
    __m256 a7 = _mm256_loadu_ps(data + (uint32)_mm_extract_epi32(offsetsHi, 3));

    // transpose
    const __m256 t0 = _mm256_unpacklo_ps(a0, a1);
    const __m256 t1 = _mm256_unpackhi_ps(a0, a1);
    const __m256 t2 = _mm256_unpacklo_ps(a2, a3);
    const __m256 t3 = _mm256_unpackhi_ps(a2, a3);
    const __m256 t4 = _mm256_unpacklo_ps(a4, a5);
    const __m256 t5 = _mm256_unpackhi_ps(a4, a5);
    const __m256 t6 = _mm256_unpacklo_ps(a6, a7);
    const __m256 t7 = _mm256_unpackhi_ps(a6, a7);
    __m256 v = _mm256_shuffle_ps(t0, t2, 0x4E);
    const __m256 tt0 = _mm256_blend_ps(t0, v, 0xCC);
    const __m256 tt1 = _mm256_blend_ps(t2, v, 0x33);
    v = _mm256_shuffle_ps(t1, t3, 0x4E);
    const __m256 tt2 = _mm256_blend_ps(t1, v, 0xCC);
    const __m256 tt3 = _mm256_blend_ps(t3, v, 0x33);
    v = _mm256_shuffle_ps(t4, t6, 0x4E);
    const __m256 tt4 = _mm256_blend_ps(t4, v, 0xCC);
    const __m256 tt5 = _mm256_blend_ps(t6, v, 0x33);
    v = _mm256_shuffle_ps(t5, t7, 0x4E);
    const __m256 tt6 = _mm256_blend_ps(t5, v, 0xCC);
    const __m256 tt7 = _mm256_blend_ps(t7, v, 0x33);
    v0 = _mm256_permute2f128_ps(tt0, tt4, 0x20);
    v1 = _mm256_permute2f128_ps(tt1, tt5, 0x20);
    v2 = _mm256_permute2f128_ps(tt2, tt6, 0x20);
    v3 = _mm256_permute2f128_ps(tt3, tt7, 0x20);
    v4 = _mm256_permute2f128_ps(tt0, tt4, 0x31);
    v5 = _mm256_permute2f128_ps(tt1, tt5, 0x31);
    v6 = _mm256_permute2f128_ps(tt2, tt6, 0x31);
    v7 = _mm256_permute2f128_ps(tt3, tt7, 0x31);
}

如您所见,我已经使用混合而不是洗牌来降低端口 5 的压力。在加载提取第一个向量元素时,我还选择了_mm_cvtsi128_si32,它只有1uop,而不是在不显眼的_mm_extract_epi32情况下为2uop。此外,手动提取较低和较高的 channel 似乎对编译器有一点帮助,并删除了冗余的 vextracti128 指令。

我尝试过使用收集指令的等效代码,正如预测的那样,速度慢了 2 倍,因为它在幕后有效地执行了 64 次加载:

void LoadTransposed_Gather(
    const float* data, __m256i offsets,
    __m256& v0, __m256& v1, __m256& v2, __m256& v3, __m256& v4, __m256& v5, __m256& v6, __m256& v7)
{
    v0 = _mm256_i32gather_ps(data + 0, offsets, 4);
    v1 = _mm256_i32gather_ps(data + 1, offsets, 4);
    v2 = _mm256_i32gather_ps(data + 2, offsets, 4);
    v3 = _mm256_i32gather_ps(data + 3, offsets, 4);
    v4 = _mm256_i32gather_ps(data + 4, offsets, 4);
    v5 = _mm256_i32gather_ps(data + 5, offsets, 4);
    v6 = _mm256_i32gather_ps(data + 6, offsets, 4);
    v7 = _mm256_i32gather_ps(data + 7, offsets, 4);
}

有什么方法可以进一步加快这个速度(前一个片段)?根据 VTune 和 IACA,最大的问题是端口 0 和 5 的高压力(可能是由于在从 __m128i 寄存器和所有 vunpckhps 偏移提取期间使用了 vpextrd在转置过程中使用 vunpcklpsvshufps)。

最佳答案

您的偏移量是否有某种模式,例如可以缩放的固定步幅?

如果没有,如果您只是需要提取它们,也许可以将它们作为结构而不是 __m256i 传递?

或者,如果您使用 SIMD 来计算偏移量(因此它们首先自然地位于 __m256i 中): 存储/重新加载到本地数组 当您需要全部时8 个元素将节省 shuffle 端口带宽。也许 _mm_cvtsi128_si32/_mm_extract_epi32(offsetsLo, 1)) 通过 ALU 操作获取前 1 或 2 个偏移量,其延迟比存储 -> 重新加载存储转发低几个周期。

例如alignas(32) uint32_t offsets[8];_mm256_store_si256 到其中。 (对于某些编译器,您可能需要阻止它“优化”到 ALU 提取中。您可以在数组上使用 volatile 作为一种讨厌的 hack 来解决这个问题。(但要小心,不要打败超过必要的优化,例如,如果您确实希望每个元素不止一次,则加载到 tmp vars 而不是多次访问 volatile 数组。这总是会击败常量传播,因为 FP 会击败诸如使用向量的低元素之类的东西作为标量,无需随机播放。)


2/时钟负载吞吐量,以及从向量存储到 32 位元素标量重新加载的高效存储转发使这一点变得很好(对于 256 位存储,可能是 7 个周期延迟 IIRC)。

特别是如果您在一个循环中执行此转置,并且其他 ALU 处理转置结果,则该循环主要在后端的端口 5 上产生瓶颈。额外的负载微指令不应成为负载端口的瓶颈,尤其是在存在任何 L1d 缓存未命中的情况下。 (在这种情况下,重放会在端口上花费额外的周期来获取消耗负载结果的指令,而不是负载微指令本身)。

前端微指令也更少:

  • 1 个存储(p237+p4 微融合)+ 1 个 vmovd (p0) + 7 个负载 (p23) 总共只有 9 个前端(融合域)uops
  • 对比vextracti128 + 2x vmovd + 6x vpextrd = 端口 0 和端口 5 的 15 个 ALU uops

在 Zen/Zen2 上存储/重新加载也很好。

IceLake 具有更高的 ALU shuffle 吞吐量(某些向量 shuffle 可以在另一个端口以及 p5 上运行),但是当您需要所有元素并且有 8 个元素时,存储/重新加载仍然是一个很好的策略。特别是对于以较小的延迟成本实现吞吐量。



@Witek902 报告(在评论中)@chtz's suggestion 使用 vmovups xmm + vinsertf128 构建转置可减少 HSW/SKL 上的端口 5 混洗吞吐量瓶颈,并在实践中提供加速vinsertf128 y,y,mem,i 对于 Intel 上的 p015 + p23 来说是 2 uops(无法微熔丝)。所以它更像是混合,不需要洗牌端口。 (它在 Bulldozer 系列/Zen1 上也非常出色,它将 YMM 寄存器处理为两个 128 位一半。)

仅执行 128 位加载对于 Sandybridge/IvyBridge 来说也很好,其中未对齐的 256 位加载非常昂贵。

并且在任何CPU上;如果偏移量恰好是 16 字节对齐的奇数倍,则 128 位加载都不会跨越高速缓存行边界。因此,依赖 ALU 微指令不会重放,从而产生额外的后端端口压力。

关于optimization - 加载和转置八个 8 元素浮点向量,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60157799/

相关文章:

x86 - 设置 AVX 寄存器(__m256i)中的各个位,需要 "random access"运算符

c - 静态与外部内在函数

php - 如何正确测试查询性能

optimization - 给定今天的时间,以 golang 中的分钟数获取 UTC 时间的最佳方式

编译器优化对使用 PAPI 的 FLOP 和 L2/L3 缓存未命中率的影响

linux - 我可以从 x64 Linux 为 x86 Windows 编写程序集吗?

c - 为什么 floor() 这么慢?

c++ - avx512 中比较内在指令的不同语义?

mysql - 需要帮助优化 mysql 的纬度/经度地理搜索

mysql - 查询优化 - WHERE 子句中的表达式顺序