我的原始数据是一堆长度 > 1000000 的(无符号)字符(8 位)c 数组。 我想按照下面代码中的规则将它们加在一起( vector 加法)。 结果: (无符号)短(16 位)的 c 数组。
我已经阅读了所有 SSE 和 AVX/AVX2 但只有一个类似的调用 那多个256位的寄存器。前 4 个 32bit 将相乘,每对 32bit 的结果是一个 64bit 将放入 256 寄存器。( _mm256_mul_epi32, _mm256_mul_epu32)
https://www.codeproject.com/Articles/874396/Crunching-Numbers-with-AVX-and-AVX
示例代码:
static inline void adder(uint16_t *canvas, uint8_t *addon, uint64_t count)
{
for (uint64_t i=0; i<count; i++)
canvas[i] += static_cast<uint16_t>(addon[i]);
}
谢谢
最佳答案
添加到@wim 答案(这是一个好答案)并考虑@Bathsheba 评论,值得信任编译器但也检查您的编译器输出既可以学习如何执行此操作,也可以检查它是否按照您的意愿进行。通过 godbolt 运行代码的略微修改版本(对于 msvc、gcc 和 clang)给出了一些不完美的答案。
如果您将自己限制在 SSE2 及低于此答案假定的值(以及我测试的内容),则尤其如此
所有编译器都对代码进行矢量化和展开,并使用 punpcklbw
将 uint8_t
“解压”为 uint16_t
,然后运行SIMD 添加和保存。这很好。但是,MSVC 往往会在内部循环中出现不必要的溢出,而 clang 仅使用 punpcklbw
而不是 punpckhbw
这意味着它会加载源数据两次。 GCC 正确处理了 SIMD 部分,但循环约束的开销更高。
因此从理论上讲,如果您想改进这些版本,您可以使用类似于以下内容的内部函数来推出自己的版本:
static inline void adder2(uint16_t *canvas, uint8_t *addon, uint64_t count)
{
uint64_t count32 = (count / 32) * 32;
__m128i zero = _mm_set_epi32(0, 0, 0, 0);
uint64_t i = 0;
for (; i < count32; i+= 32)
{
uint8_t* addonAddress = (addon + i);
// Load data 32 bytes at a time and widen the input
// to `uint16_t`'sinto 4 temp xmm reigsters.
__m128i input = _mm_loadu_si128((__m128i*)(addonAddress + 0));
__m128i temp1 = _mm_unpacklo_epi8(input, zero);
__m128i temp2 = _mm_unpackhi_epi8(input, zero);
__m128i input2 = _mm_loadu_si128((__m128i*)(addonAddress + 16));
__m128i temp3 = _mm_unpacklo_epi8(input2, zero);
__m128i temp4 = _mm_unpackhi_epi8(input2, zero);
// Load data we need to update
uint16_t* canvasAddress = (canvas + i);
__m128i canvas1 = _mm_loadu_si128((__m128i*)(canvasAddress + 0));
__m128i canvas2 = _mm_loadu_si128((__m128i*)(canvasAddress + 8));
__m128i canvas3 = _mm_loadu_si128((__m128i*)(canvasAddress + 16));
__m128i canvas4 = _mm_loadu_si128((__m128i*)(canvasAddress + 24));
// Update the values
__m128i output1 = _mm_add_epi16(canvas1, temp1);
__m128i output2 = _mm_add_epi16(canvas2, temp2);
__m128i output3 = _mm_add_epi16(canvas3, temp3);
__m128i output4 = _mm_add_epi16(canvas4, temp4);
// Store the values
_mm_storeu_si128((__m128i*)(canvasAddress + 0), output1);
_mm_storeu_si128((__m128i*)(canvasAddress + 8), output2);
_mm_storeu_si128((__m128i*)(canvasAddress + 16), output3);
_mm_storeu_si128((__m128i*)(canvasAddress + 24), output4);
}
// Mop up
for (; i<count; i++)
canvas[i] += static_cast<uint16_t>(addon[i]);
}
检查它的输出,它比 gcc/clang/msvc 中的任何一个都要好。所以如果你想获得绝对的最后一滴性能(并且有一个固定的架构)那么像上面这样的东西是可能的。 但是这是一个非常小的改进,因为编译器已经几乎完美地处理了这个问题,所以我实际上建议不要这样做,而只是相信编译器。
如果您确实认为可以改进编译器,请记住始终进行测试和分析以确保您确实可以。
关于c++ - 将大型 char8 c 数组转换为 short16 的最快方法是什么?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54233865/