首页 > 解决方案 > 在循环中广播 SIMD 寄存器的每个元素

问题描述

我需要用另一个 SIMD 寄存器的一个元素填充一个 SIMD 寄存器。即“广播”或“splat”一个元素到每个位置。

我目前的代码是(它被简化了,我的真实函数被声明了inline):

__m128
f4_broadcast_1(__m128 a, int i) {
    return _mm_set1_ps(a[i]);
}

这似乎在 clang 和 gcc 上生成了有效的代码,但 msvc 禁止索引访问。因此,我改为写:

__m128
f4_broadcast_2(__m128 a, int i) {
    union { __m128 reg; float f[4]; } r = { .reg = a };
    return _mm_set1_ps(r.f[i]);
}

它在 clang 和 gcc 上生成相同的代码,但在 msvc 上生成错误代码。神螺栓链接: https ://godbolt.org/z/IlOqZl

有更好的方法吗?我知道关于 SO 已经有类似的问题,但是我的用例涉及从寄存器中提取 float32 并将其放回另一个寄存器中,这是一个略有不同的问题。如果您可以在完全不必触及主存储器的情况下做到这一点,那就太酷了。

索引是变量还是常量?显然,它是否对 SIMD 性能很重要。在我的例子中,索引是一个循环变量:

for (int i = 0; i < M; i++) {
    ... broadcast element i of some reg
}

其中 M 是 4、8 或 16。也许我应该手动展开循环以使其成为常数?for循环中有很多代码,因此代码量会大大增加。

我也想知道如何做同样的事情,除了现代 cpu:s 上的__m256and寄存器。__m512

标签: cperformancessesimdavx

解决方案


在运行时从 simd 寄存器中获取任意浮点数中的一些洗牌?可以适应广播一个元素,而不是如果它到低元素则只获得 1 个副本。它更详细地讨论了 shuffle 与 store/reload 策略的权衡。


vpermilpsx86 在 AVX和 AVX2 车道交叉vpermps/之前没有 32 位元素可变控制随机播放vpermd。例如

// for runtime-variable i.  Otherwise use something more efficient.
_mm_permutevar_ps(v, _mm_set1_epi32(i));

vbroadcastss或者用(矢量源版本需要AVX2)广播低元素

使用 AVX1 的广播负载非常有效:(_mm_broadcast_ss(float*)_mm256/512相同的)或只是 128/256/512_mm_set1_ps(float)的浮点数,恰好来自内存,如果启用 AVX1 进行编译,则让您的编译器使用广播负载。


使用 compile-time-constant 控件,您可以使用 SSE1 广播任何单个元素
_mm_shuffle_ps(same,same, _MM_SHUFFLE(i,i,i,i));

或者对于整数,使用 SSE2 pshufd: _mm_shuffle_epi32(v, _MM_SHUFFLE(i,i,i,i))

根据您的编译器,它可能必须是一个宏,i才能成为禁用优化的编译时常量。shuffle-control 常量必须编译成嵌入在机器代码中的立即字节(带有 4 个 2 位字段),而不是作为数据加载或从寄存器加载。


循环遍历元素。

我在本节中使用 AVX2;这很容易适应 AVX512。如果没有 AVX2,存储/重新加载策略是 256 位向量或vpermilps128 位向量的唯一好选择。

可能为 SSSE3 增加计数器(增加 4) (在andpshufb之间进行转换)`在没有 AVX 的情况下可能是一个好主意,因为您没有有效的广播负载。__m128i__m128

索引是一个循环变量

编译器通常会为您完全展开循环,将循环变量转换为每次迭代的编译时常量。但只有启用优化。在 C++ 中,您可以使用模板递归来迭代constexpr.

MSVC 不会优化内在函数,所以如果你编写_mm_permutevar_ps(v, _mm_set1_epi32(i));你实际上会在每次迭代中得到它,而不是4x vshufps。但是 gcc 尤其是 clang 确实优化了 shuffle,所以它们应该在启用优化的情况下做得很好。

for循环中有很多代码

如果需要大量寄存器/花费大量时间,则存储/重新加载可能是一个不错的选择,尤其是在 AVX 可用于广播重新加载的情况下。在当前 Intel CPU 上,Shuffle 吞吐量(1/时钟)比负载吞吐量(2/时钟)更受限制。

使用 AVX512 编译代码甚至允许广播内存源操作数,而不是单独的加载指令,因此如果只需要一次,编译器甚至可以将广播加载折叠到源操作数中。

/*********   Store/reload strategy ****************/
#include <stdalign.h>

void foo(__m256 v) {
   alignas(32)  float tmp[8];
   _mm256_store_ps(tmp, v);

   // with only AVX1, maybe don't peel first iteration, or broadcast manually in 2 steps
   __m256 bcast = _mm256_broadcastss_ps(_mm256_castps256_ps128(v));  // AVX2 vbroadcastss ymm, xmm
    ... do stuff with bcast ...

    for (int i=1; i<8 ; i++) {
        bcast = _mm256_broadcast_ss(tmp[i]);
        ... do stuff with bcast ...
    }
}

我手动剥离了第一次迭代,只使用 ALU 操作(较低延迟)广播低元素,以便它可以立即开始。随后的迭代然后使用广播负载重新加载。

如果您有 AVX2,另一种选择是使用 SIMD 增量进行矢量随机控制(又名掩码)。

// Also AVX2
void foo(__m256 v) {

   __m256i shufmask = _mm256_setzero_si256();

    for (int i=1; i<8 ; i++) {
        __m256 bcast = _mm256_permutevar8x32_ps(v, shufmask);    // AVX2 vpermps
        // prep for next iteration by incrementing the element selectors
        shufmask = _mm256_add_epi32(shufmask, _mm256_set1_epi32(1));

        ... do stuff with bcast ...

    }
}

vpaddd在 shufmask 上做了一个冗余(在最后一次迭代中),但这可能比剥离第一次或最后一次迭代更好并且更好。并且显然比-1在第一次迭代中在随机播放之前开始并进行添加要好。

车道交叉 shuffle 在 Intel 上具有 3 个周期的延迟,因此将其放在 shuffle 之后可能是很好的调度,除非有其他不依赖于的每次迭代工作bcast;无论如何,乱序执行使这成为一个小问题。在第一次迭代中,vpermps使用 xor-zeroed 的掩码基本上与 Intel 一样好vbroadcastss,以便乱序 exec 快速启动。

但是在 AMD CPU 上(至少在 Zen2 之前),车道交叉vpermps非常慢;粒度 <128 位的通道交叉洗牌非常昂贵,因为它必须解码为 128 位微指令。所以这个策略对 AMD 来说并不好。如果存储/重新加载在 Intel 上对您周围的代码执行相同的操作,那么让您的代码也对 AMD 友好可能是一个更好的选择。

vpermpsAVX512 内部函数还引入了一个新的内部函数:_mm256_permutexvar_ps(__m256i idx, __m256 a)它的操作数的顺序与 asm 匹配。如果您的编译器支持新的,请使用您喜欢的任何一个。


推荐阅读