我的原始数据是一串c数组(长度为1000000)的(无符号)char(8位)。 我想将它们加在一起(矢量加法),遵循以下代码中的规则。 结果: (无符号)短(16位)的c数组。
我已经阅读了所有SSE和AVX / AVX2,但那里只有一个类似的电话 2个256bit的寄存器。前4个32位将相乘,每对32位的结果是一个64位,将适合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]);
}
谢谢
答案 0 :(得分:4)
实际上,注释是正确的:编译器可以为您进行矢量化。
我对您的代码进行了一些修改,以改善自动矢量化。
对于gcc -O3 -march=haswell -std=c++14
(gcc版本8.2),以下代码:
#include <cstdint>
#include <immintrin.h>
void cvt_uint8_int16(uint16_t * __restrict__ canvas, uint8_t * __restrict__ addon, int64_t count) {
int64_t i;
/* If you know that n is always a multiple of 32 then insert */
/* n = n & 0xFFFFFFFFFFFFFFE0u; */
/* This leads to cleaner code. Now assume n is a multiple of 32: */
count = count & 0xFFFFFFFFFFFFFFE0u;
for (i = 0; i < count; i++){
canvas[i] += static_cast<uint16_t>(addon[i]);
}
}
编译为:
cvt_uint8_int16(unsigned short*, unsigned char*, long):
and rdx, -32
jle .L5
add rdx, rsi
.L3:
vmovdqu ymm2, YMMWORD PTR [rsi]
add rsi, 32
add rdi, 64
vextracti128 xmm1, ymm2, 0x1
vpmovzxbw ymm0, xmm2
vpaddw ymm0, ymm0, YMMWORD PTR [rdi-64]
vpmovzxbw ymm1, xmm1
vpaddw ymm1, ymm1, YMMWORD PTR [rdi-32]
vmovdqu YMMWORD PTR [rdi-64], ymm0
vmovdqu YMMWORD PTR [rdi-32], ymm1
cmp rdx, rsi
jne .L3
vzeroupper
.L5:
编译器Clang产生的code有点不同:它加载128位(字符)向量,并用vpmovzxbw
进行转换。
编译器gcc加载256位(char)向量,并转换高128位和低128位
分别,效率可能会略低。
但是,无论如何,您的问题可能是带宽有限(因为长度> 1000000)。
您还可以使用内在函数对代码进行矢量化(未测试):
void cvt_uint8_int16_with_intrinsics(uint16_t * __restrict__ canvas, uint8_t * __restrict__ addon, int64_t count) {
int64_t i;
/* Assume n is a multiple of 16 */
for (i = 0; i < count; i=i+16){
__m128i x = _mm_loadu_si128((__m128i*)&addon[i]);
__m256i y = _mm256_loadu_si256((__m256i*)&canvas[i]);
__m256i x_u16 = _mm256_cvtepu8_epi16(x);
__m256i sum = _mm256_add_epi16(y, x_u16);
_mm256_storeu_si256((__m256i*)&canvas[i], sum);
}
}
这导致与results类似的自动矢量化代码。
答案 1 :(得分:4)
添加到@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中的任何一个都要严格。因此,如果您想获得绝对的性能下降(并且具有固定的体系结构),则可能会出现上述情况。 但是的确是很小的改进,因为编译器已经可以非常完美地处理此问题了,所以我实际上建议不要这样做,而应该信任编译器。
如果您确实认为可以改善编译器,请记住始终进行测试和配置文件以确保您确实是。
答案 2 :(得分:3)
与wim和Mike的出色答案中介绍的手动优化方法相反,让我们快速了解一下完全原始的C ++实现将为我们带来什么:
std::transform(addon, addon + count, canvas, canvas, std::plus<void>());
Try it out here。您会发现,即使您不花力气,编译器也已经能够生成很好的矢量化代码,因为它不能对缓冲区的对齐方式和大小做任何假设,而且还存在一些潜在的别名问题(由于使用了uint8_t
,不幸的是,这迫使编译器假定指针可以别名为任何其他对象。另外,请注意,该代码与从C样式实现中获得的代码基本相同(取决于编译器,C ++版本的指令多或少。)
void f(uint16_t* canvas, const uint8_t* addon, size_t count)
{
for (size_t i = 0; i < count; ++i)
canvas[i] += addon[i];
}
但是,只要可以添加元素类型,通用C ++解决方案就可以对不同种类的容器和元素类型进行任意组合。因此,正如在其他答案中所指出的那样,虽然当然有可能通过手动优化获得稍微更有效的实现,但是仅通过编写简单的C ++代码(如果做得正确)就可以走很长一段路。在诉诸于手动编写SSE内部函数之前,请考虑通用C ++解决方案更灵活,更易于维护,尤其是更具可移植性。通过简单切换目标体系结构,您不仅可以为SSE生成质量相似的代码,还可以为AVX甚至是带有NEON的ARM以及可能碰巧要运行的任何其他指令集生成代码。如果您需要代码在一个特定的CPU上完美到最后一个指令的最后一条指令,那么是的,内在函数甚至是内联汇编都是可行的方法。但总的来说,我还建议改用以使能和指导编译器生成所需程序集的方式来编写C ++代码,而不是自己生成程序集。例如,通过使用(非标准但普遍可用的)限制限定符并通过使编译器知道您的count
始终是32的倍数来借用该技巧
void f(std::uint16_t* __restrict__ canvas, const std::uint8_t* __restrict__ addon, std::size_t count)
{
assert(count % 32 == 0);
count = count & -32;
std::transform(addon, addon + count, canvas, canvas, std::plus<void>());
}
you get(-std=c++17 -DNDEBUG -O3 -mavx
)
f(unsigned short*, unsigned char const*, unsigned long):
and rdx, -32
je .LBB0_3
xor eax, eax
.LBB0_2: # =>This Inner Loop Header: Depth=1
vpmovzxbw xmm0, qword ptr [rsi + rax] # xmm0 = mem[0],zero,mem[1],zero,mem[2],zero,mem[3],zero,mem[4],zero,mem[5],zero,mem[6],zero,mem[7],zero
vpmovzxbw xmm1, qword ptr [rsi + rax + 8] # xmm1 = mem[0],zero,mem[1],zero,mem[2],zero,mem[3],zero,mem[4],zero,mem[5],zero,mem[6],zero,mem[7],zero
vpmovzxbw xmm2, qword ptr [rsi + rax + 16] # xmm2 = mem[0],zero,mem[1],zero,mem[2],zero,mem[3],zero,mem[4],zero,mem[5],zero,mem[6],zero,mem[7],zero
vpmovzxbw xmm3, qword ptr [rsi + rax + 24] # xmm3 = mem[0],zero,mem[1],zero,mem[2],zero,mem[3],zero,mem[4],zero,mem[5],zero,mem[6],zero,mem[7],zero
vpaddw xmm0, xmm0, xmmword ptr [rdi + 2*rax]
vpaddw xmm1, xmm1, xmmword ptr [rdi + 2*rax + 16]
vpaddw xmm2, xmm2, xmmword ptr [rdi + 2*rax + 32]
vpaddw xmm3, xmm3, xmmword ptr [rdi + 2*rax + 48]
vmovdqu xmmword ptr [rdi + 2*rax], xmm0
vmovdqu xmmword ptr [rdi + 2*rax + 16], xmm1
vmovdqu xmmword ptr [rdi + 2*rax + 32], xmm2
vmovdqu xmmword ptr [rdi + 2*rax + 48], xmm3
add rax, 32
cmp rdx, rax
jne .LBB0_2
.LBB0_3:
ret
这还不错...