SIMD (AVX2) - 将 uint8_t 值加载到多个浮点 __m256 寄存器
Posted
技术标签:
【中文标题】SIMD (AVX2) - 将 uint8_t 值加载到多个浮点 __m256 寄存器【英文标题】:SIMD (AVX2) - load uint8_t values to multiple float __m256 registers 【发布时间】:2021-03-19 09:46:35 【问题描述】:我有来自uint8_t
值的灰度图像。我想将数据加载到 SIMD。我加载了 16 个值并将它们转换为两个 __m256
浮点寄存器。
我用:
uint8_t * data = .....
size_t index = ....
//load 16 uint8_t (16 * 8 = 128bit)
__m128i val = _mm_loadu_si128((const __m128i*)(data + index));
//convert to 16bit int
__m128i lo16 = _mm_unpacklo_epi8(val, _mm_setzero_si128());
__m128i hi16 = _mm_unpackhi_epi8(val, _mm_setzero_si128());
//convert to 32bit int
__m256i lo32 = _mm256_cvtepi16_epi32(lo16);
__m256i hi32 = _mm256_cvtepi16_epi32(hi16);
//convert to float
__m256 lo = _mm256_cvtepi32_ps(lo32);
__m256 hi = _mm256_cvtepi32_ps(hi32);
有没有比我的解决方案更好的方法(没有 AVX-512)?
加载_mm256_loadu_si256
并将其“拆分”到4 个__m256
寄存器会更好吗?
【问题讨论】:
【参考方案1】:打开_mm256_loadu_si256
的高通道将花费更多的洗牌。大多数 AVX2 CPU 的负载吞吐量高于 shuffle 吞吐量,并且您已经需要每个输出向量至少 1 次 shuffle,因此您对 2 个 epi32
向量执行 1 次加载和 4 次 shuffle 的方式已经是一个糟糕的权衡。
如果有的话,最好使用 2x _mm256_cvtepu8_epi32
来获得 cvtepi32_ps 的两个输入向量,每次随机播放一个负载。
使用内存源pmovz/sx
有点痛苦,因为您需要告诉编译器您正在对__m128i
进行窄加载(为了安全起见),并且一些编译器不会优化将零扩展加载到vpmovzx
的内存源中。 见Loading 8 chars from memory into an __m256 variable as packed single precision floats
但显然,自从我在 2015 年最初写下这个答案以来,情况已经有所改善; GCC9 及更高版本修复了错过优化的错误,现在将_mm_loadl_epi64( (const __m128i*)p)
折叠到vpmovzx
的内存源中。 clang 和 ICC 都很好,即使是旧版本。 MSVC 仍然使用单独的vmovq
进行糟糕的代码生成,即使使用-march:AVX2
,甚至是 v19.28 和“最新”。 (Godbolt)。
在 Intel CPU 上 vpmovzxbd ymm, qword [mem]
始终为 2 uop;无法对负载进行微融合(仅适用于 xmm 目标)https://uops.info/table.html,因此即使编译器确实设法将 64 位内存源折叠为 mem 操作数,您也不会获得任何东西(代码大小除外)而不是使用vmovq
加载。
但在 Zen2 上,该指令的吞吐量为 2/时钟吞吐量,而 vpmovzxbd ymm, xmm
的吞吐量低于 1/时钟吞吐量(wd
shuffle 或您正在使用的符号扩展版本 vpmovsxwd 相同= Epi16_epi32)。因此,如果您关心 Zen CPU,尤其是 Zen 2,您确实希望编译器能够正确处理此问题。
【讨论】:
以上是关于SIMD (AVX2) - 将 uint8_t 值加载到多个浮点 __m256 寄存器的主要内容,如果未能解决你的问题,请参考以下文章