c - 在循环中广播 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 上的__m256
and寄存器。__m512
解决方案
在运行时从 simd 寄存器中获取任意浮点数中的一些洗牌?可以适应广播一个元素,而不是如果它到低元素则只获得 1 个副本。它更详细地讨论了 shuffle 与 store/reload 策略的权衡。
vpermilps
x86 在 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 位向量或vpermilps
128 位向量的唯一好选择。
可能为 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 友好可能是一个更好的选择。
vpermps
AVX512 内部函数还引入了一个新的内部函数:_mm256_permutexvar_ps(__m256i idx, __m256 a)
它的操作数的顺序与 asm 匹配。如果您的编译器支持新的,请使用您喜欢的任何一个。
推荐阅读
- c++ - 尝试在 xcode 中退出程序时出现 C++ 11db 错误。初级水平
- vue.js - 下拉元素(v-overflow-btn、v-date-picker)仅部分显示
- python - 我可以创建一个带有根目录的包吗?
- python - 双重迭代不会为dict产生错误?
- python - Python 运行速度明显快于 C++?这里似乎有问题
- field - 脚本是否可以引用新记录上的一行?
- css - 如何在 css 中获取此元素
- fullcalendar - 单击全日历上的日期时,我收到未捕获的类型错误
- php - Laravel Morph 关系 - 分离/附加
- python - 如何解决关于 Tensorflow 1.14 配置的“在任何子目录中找不到任何 libcudnn.7*.dylib:”