在循环中广播 SIMD 寄存器的每个元素
Broadcasting each element of a SIMD register in a loop
我需要用另一个 SIMD 寄存器的一个元素填充一个 SIMD 寄存器。即 "broadcast" 或 "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 上生成错误代码。神箭 link: 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.
上的 __m256
和 __m512
寄存器如何做同样的事情
广播可以使用AVX2指令实现VBROADCASTSS
,但是将值移动到输入位置(第一个位置)取决于你的指令集:
VBROADCASTSS (128 bit version VEX and legacy)
该指令将源 XMM 寄存器位置 [0]
上的源值广播到目标 XMM 寄存器的所有四个 FLOATS。它的内在是 __m128 _mm_broadcastss_ps(__m128 a);
.
如果你的值的位置是不变的,你可以使用指令PSHUFD
将值从当前位置移动到第一个位置。它的内在是__m128i _mm_shuffle_epi32(__m128i a, int n)
。要将应广播的值移动到输入 XMM 向量的第一个位置,请对 int n
使用以下值:
1. : 0h
2. : 1h
3. : 2h
4. : 3h
这会将值从 0..3 位置移动到第一个位置。
因此,例如,使用以下内容将 input
向量的第四个位置移动到第一个:
__m128 newInput = _mm_shuffle_epi32(__m128i input, 3)
然后应用以下内在函数:
__m128 result = _mm_broadcastss_ps(__m128 newInput);
现在 input
XMM 向量的第四个位置的值应该在 result
向量的所有位置上。
中的一些 shuffle 可以调整为广播一个元素,而不是只获取 1 个副本(如果它是低元素)。它更详细地讨论了随机播放与 store/reload 策略的权衡。
x86 在 AVX vpermilps
和 AVX2 车道交叉 vpermps
/ vpermd
之前没有 32 位元素变量控制洗牌。例如
// 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 的情况下进行编译,则让您的编译器使用广播加载。
使用编译时间常数控件,您可以使用 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 常量必须编译成嵌入机器代码中的立即字节(具有 4x 2 位字段),而不是作为数据或从寄存器加载。
在循环中迭代元素。
我在这部分使用的是 AVX2;这很容易适应 AVX512。如果没有 AVX2,store/reload 策略是 256 位向量的唯一好选择,或者 vpermilps
是 128 位向量的唯一好选择。
可能为 SSSE3 pshufb
(在 __m128i
和 __m128
之间进行转换)增加计数器(增加 4)`在没有 AVX 的情况下可能是个好主意有效的广播负载。
the index is a loop variable
编译器通常会为您完全展开循环,将循环变量转换为每次迭代的编译时常量。但只有在启用优化的情况下。在 C++ 中,您可以使用模板递归来迭代 constexpr
.
MSVC 不优化内在函数,所以如果你写 _mm_permutevar_ps(v, _mm_set1_epi32(i));
你实际上会在每次迭代中得到它,不是 4x vshufps
.但是 gcc,尤其是 clang 确实优化了洗牌,所以他们应该在启用优化的情况下做得很好。
It's a lot of code in the for-loop
如果需要大量寄存器/花费大量时间,store/reload 可能是一个不错的选择,尤其是在 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 操作(较低的延迟)广播低元素,以便它可以立即开始。之后的迭代然后使用广播负载重新加载。
另一种选择是使用 SIMD 增量进行矢量随机播放控制(也称为掩码),如果您有 AVX2。
// 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 ...
}
}
这在 shufmask 上做了一个冗余 vpaddd
(在最后一次迭代中),但这可能很好并且比剥离第一次或最后一次迭代更好。显然比从 -1
开始并在第一次迭代中的随机播放之前进行添加要好。
车道交叉洗牌在 Intel 上有 3 个周期的延迟,所以将它放在洗牌之后可能是很好的调度,除非有其他不依赖于 bcast
的每次迭代工作;无论如何,乱序执行使这成为一个小问题。在第一次迭代中,vpermps
的掩码只是异或归零,基本上与 Intel 上的 vbroadcastss
一样好,乱序执行可以快速启动。
但是在 AMD CPU 上(至少在 Zen2 之前),车道交叉 vpermps
非常慢;粒度小于 128 位的跨车道洗牌非常昂贵,因为它必须解码为 128 位微指令。所以这个策略对 AMD 来说并不好。如果 store/reload 在 Intel 上对您周围的代码执行同样的操作,那么让您的代码也对 AMD 友好可能是更好的选择。
vpermps
还引入了一个与 AVX512 内在函数一起引入的新内在函数:_mm256_permutexvar_ps(__m256i idx, __m256 a)
,其操作数的顺序与 asm 匹配。如果您的编译器支持新版本,请使用您喜欢的任何一个。
我需要用另一个 SIMD 寄存器的一个元素填充一个 SIMD 寄存器。即 "broadcast" 或 "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 上生成错误代码。神箭 link: 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.
上的__m256
和 __m512
寄存器如何做同样的事情
广播可以使用AVX2指令实现VBROADCASTSS
,但是将值移动到输入位置(第一个位置)取决于你的指令集:
VBROADCASTSS (128 bit version VEX and legacy)
该指令将源 XMM 寄存器位置 [0]
上的源值广播到目标 XMM 寄存器的所有四个 FLOATS。它的内在是 __m128 _mm_broadcastss_ps(__m128 a);
.
如果你的值的位置是不变的,你可以使用指令PSHUFD
将值从当前位置移动到第一个位置。它的内在是__m128i _mm_shuffle_epi32(__m128i a, int n)
。要将应广播的值移动到输入 XMM 向量的第一个位置,请对 int n
使用以下值:
1. : 0h
2. : 1h
3. : 2h
4. : 3h
这会将值从 0..3 位置移动到第一个位置。
因此,例如,使用以下内容将 input
向量的第四个位置移动到第一个:
__m128 newInput = _mm_shuffle_epi32(__m128i input, 3)
然后应用以下内在函数:
__m128 result = _mm_broadcastss_ps(__m128 newInput);
现在 input
XMM 向量的第四个位置的值应该在 result
向量的所有位置上。
x86 在 AVX vpermilps
和 AVX2 车道交叉 vpermps
/ vpermd
之前没有 32 位元素变量控制洗牌。例如
// 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 的情况下进行编译,则让您的编译器使用广播加载。
使用编译时间常数控件,您可以使用 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 常量必须编译成嵌入机器代码中的立即字节(具有 4x 2 位字段),而不是作为数据或从寄存器加载。
在循环中迭代元素。
我在这部分使用的是 AVX2;这很容易适应 AVX512。如果没有 AVX2,store/reload 策略是 256 位向量的唯一好选择,或者 vpermilps
是 128 位向量的唯一好选择。
可能为 SSSE3 pshufb
(在 __m128i
和 __m128
之间进行转换)增加计数器(增加 4)`在没有 AVX 的情况下可能是个好主意有效的广播负载。
the index is a loop variable
编译器通常会为您完全展开循环,将循环变量转换为每次迭代的编译时常量。但只有在启用优化的情况下。在 C++ 中,您可以使用模板递归来迭代 constexpr
.
MSVC 不优化内在函数,所以如果你写 _mm_permutevar_ps(v, _mm_set1_epi32(i));
你实际上会在每次迭代中得到它,不是 4x vshufps
.但是 gcc,尤其是 clang 确实优化了洗牌,所以他们应该在启用优化的情况下做得很好。
It's a lot of code in the for-loop
如果需要大量寄存器/花费大量时间,store/reload 可能是一个不错的选择,尤其是在 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 操作(较低的延迟)广播低元素,以便它可以立即开始。之后的迭代然后使用广播负载重新加载。
另一种选择是使用 SIMD 增量进行矢量随机播放控制(也称为掩码),如果您有 AVX2。
// 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 ...
}
}
这在 shufmask 上做了一个冗余 vpaddd
(在最后一次迭代中),但这可能很好并且比剥离第一次或最后一次迭代更好。显然比从 -1
开始并在第一次迭代中的随机播放之前进行添加要好。
车道交叉洗牌在 Intel 上有 3 个周期的延迟,所以将它放在洗牌之后可能是很好的调度,除非有其他不依赖于 bcast
的每次迭代工作;无论如何,乱序执行使这成为一个小问题。在第一次迭代中,vpermps
的掩码只是异或归零,基本上与 Intel 上的 vbroadcastss
一样好,乱序执行可以快速启动。
但是在 AMD CPU 上(至少在 Zen2 之前),车道交叉 vpermps
非常慢;粒度小于 128 位的跨车道洗牌非常昂贵,因为它必须解码为 128 位微指令。所以这个策略对 AMD 来说并不好。如果 store/reload 在 Intel 上对您周围的代码执行同样的操作,那么让您的代码也对 AMD 友好可能是更好的选择。
vpermps
还引入了一个与 AVX512 内在函数一起引入的新内在函数:_mm256_permutexvar_ps(__m256i idx, __m256 a)
,其操作数的顺序与 asm 匹配。如果您的编译器支持新版本,请使用您喜欢的任何一个。