将char8的大型c数组转换为short16的最快方法是什么
What is the fastest way to convert a large c-array of char8 to short16?
我的原始数据是一堆长度>100000的(无符号)字符(8位)的c数组。我想把它们加在一起(矢量加法),遵循下面代码中的规则。结果:c—(无符号)短(16位)的数组。
我已经阅读了所有的SSE和AVX/AVX2,但只有一个类似的调用多个256位的2个寄存器。前4个32位将相乘,每对32位的结果是64位将适合256寄存器。(_mm256-mul_epi32,_mm256-ul_epu32)
Firgure
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,并且此答案假设(以及我测试的内容)低于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中的任何一个都要好。因此,如果你想获得绝对的最后一滴perf(并有一个固定的体系结构),那么像上面这样的东西是可能的然而这是一个非常小的改进,因为编译器已经几乎完美地处理了,所以我实际上建议不要这样做,只相信编译器。
如果您确实认为可以改进编译器,请记住始终进行测试和评测,以确保您确实做到了。
事实上,注释是正确的:编译器可以为您进行矢量化。我对您的代码进行了一些修改,以改进自动向量化。对于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生成的代码有点不同:它加载128位(char)向量,并用vpmovzxbw
进行转换。编译器gcc加载256位(char)矢量,并转换高位和低位128位这可能稍微不那么有效。然而,您的问题可能是带宽受限(因为长度>100000)。
您还可以使用内部函数(未测试)对代码进行矢量化:
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);
}
}
这将导致与自动向量化代码类似的结果。
与wim和Mike的精彩答案中提出的手动优化方法相比,让我们快速了解一下一个完全普通的C++实现会给我们带来什么:
std::transform(addon, addon + count, canvas, canvas, std::plus<void>());
在这里试试。您将看到,即使您没有付出任何实际的努力,编译器也已经能够生成非常好的矢量化代码,因为它不能对缓冲区的对齐和大小做出任何假设,而且还存在一些潜在的混叠问题(由于使用了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上的一个特定用例的最后一条指令,那么是的,intrinsic甚至内联程序集可能是最好的选择。但总的来说,我也建议将重点放在编写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>());
}
你得到(-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
这真的还不错…
- 为不同配置设置MSVC_RUNTIME_LIBRARY的正确方法是什么
- 通过方法访问结构
- 最小硬币更换问题(自上而下方法)
- C++为构建时间获取QDateTime的可靠方法
- 在C#中处理C++指针而不使用unsafe的最佳方法
- 处理多个异常集合的C++方法
- 如果C++类在类方法中具有动态分配,但没有构造函数/析构函数或任何非静态成员,那么它仍然是POD类型吗
- 有什么方法可以遍历结构吗
- 当类在C++中定义时,有什么方法可以"register"类吗?
- 在C++中,将大的无符号浮点数四舍五入为整数的最佳方法是什么
- 实现无开销push_back的最佳方法是什么
- 使用std::函数映射对象方法
- 有符号的int和int-有没有一种方法可以在C++中区分它们
- C++从另一个类访问公共静态向量的正确方法是什么
- C++优先级队列,按对象的唯一指针的特定方法升序排列
- 没有为自己的结构调用列表推回方法
- 有没有什么方法可以使用一个函数中定义的常量变量,也可以由c++中同一程序中的其他函数使用
- 在类定义之后定义一个私有方法
- 枚举环境变量的惯用C++14/C++17方法
- 将char8的大型c数组转换为short16的最快方法是什么