在运行 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/