我需要一种快速的方法将每个第二个字节复制到一个新的malloc'd内存区域中。我有一个原始图像,其中包含RGB数据和每个通道16位(48位),并且希望创建一个每个通道8位(24位)的RGB图像。
是否有比逐字节复制更快的方法?我对SSE2不是很了解,但我认为使用SSE / SSE2可能是可能的。
我需要一种快速的方法将每个第二个字节复制到一个新的malloc'd内存区域中。我有一个原始图像,其中包含RGB数据和每个通道16位(48位),并且希望创建一个每个通道8位(24位)的RGB图像。
是否有比逐字节复制更快的方法?我对SSE2不是很了解,但我认为使用SSE / SSE2可能是可能的。
packuswb
)可能是一个胜利。但由于它饱和而不是丢弃您不想要的字节,因此您必须提供具有每个16位元素低字节中要保留数据的输入。在您的情况下,这可能是每个RGB16组件的高字节,从每个颜色分量中舍弃8个最低有效位。即_mm_srli_epi16(v, 8)
。要将每个16位元素中的高字节清零,请改用_mm_and_si128(v, _mm_set1_epi16(0x00ff))
。(在这种情况下,不要再考虑使用非对齐加载来替换其中一个移位;这是容易的情况,您应该只使用两个AND来提供PACKUS。)
-O3
下的自动向量化方式,除了它们都会出现问题并浪费很多指令(https://gcc.gnu.org/bugzilla/show_bug.cgi?id=82356,https://bugs.llvm.org/show_bug.cgi?id=34773)。不过,让它们使用SSE2(x86-64的基准线)或者ARM的NEON等进行自动向量化,是一种安全的获取性能的方法,而不用担心手动向量化可能引入的错误。除了编译器的问题,它们生成的任何内容都将正确实现此代码的C语义,适用于任何大小和对齐方式:// gcc and clang both auto-vectorize this sub-optimally with SSE2.
// clang is *really* sub-optimal with AVX2, gcc no worse
void pack_high8_baseline(uint8_t *__restrict__ dst, const uint16_t *__restrict__ src, size_t bytes) {
uint8_t *end_dst = dst + bytes;
do{
*dst++ = *src++ >> 8;
} while(dst < end_dst);
}
查看此版本以及后续版本的代码和汇编在Godbolt上。
// Compilers auto-vectorize sort of like this, but with different
// silly missed optimizations.
// This is a sort of reasonable SSE2 baseline with no manual unrolling.
void pack_high8(uint8_t *restrict dst, const uint16_t *restrict src, size_t bytes) {
// TODO: handle non-multiple-of-16 sizes
uint8_t *end_dst = dst + bytes;
do{
__m128i v0 = _mm_loadu_si128((__m128i*)src);
__m128i v1 = _mm_loadu_si128(((__m128i*)src)+1);
v0 = _mm_srli_epi16(v0, 8);
v1 = _mm_srli_epi16(v1, 8);
__m128i pack = _mm_packus_epi16(v0, v1);
_mm_storeu_si128((__m128i*)dst, pack);
dst += 16;
src += 16; // 32 bytes, unsigned short
} while(dst < end_dst);
}
// take both args as uint8_t* so we can offset by 1 byte to replace a shift with an AND
// if src is 32B-aligned, we never have cache-line splits
void pack_high8_alignhack(uint8_t *restrict dst, const uint8_t *restrict src, size_t bytes) {
uint8_t *end_dst = dst + bytes;
do{
__m128i v0 = _mm_loadu_si128((__m128i*)src);
__m128i v1_offset = _mm_loadu_si128(1+(__m128i*)(src-1));
v0 = _mm_srli_epi16(v0, 8);
__m128i v1 = _mm_and_si128(v1_offset, _mm_set1_epi16(0x00FF));
__m128i pack = _mm_packus_epi16(v0, v1);
_mm_store_si128((__m128i*)dst, pack);
dst += 16;
src += 32; // 32 bytes
} while(dst < end_dst);
}
gcc -O3 -funroll-loops
对于此手动矢量化版本看起来非常好,特别是使用gcc -O3 -funroll-loops -march=sandybridge
启用AVX。
有了AVX,可能值得同时使用v0
和v1
进行and
,以减少前端瓶颈,代价是具有缓存行拆分(和偶尔的页面拆分)。但可能取决于uarch以及您的数据是否已经错位。 (在这方面进行分支可能是值得的,因为如果数据在L1D中很热,则需要最大化缓存带宽)。
使用AVX2,256b版本的256b加载应该在Haswell/Skylake上运行良好。如果src
按64B对齐,则偏移加载仍不会分裂缓存行。(它将始终加载缓存行的字节[62:31]
,而v0
加载将始终加载字节[31:0]
)。但是,在128b通道内进行打包工作,因此在打包后,必须使用vpermq
进行洗牌,以将64位块放入正确的顺序中。查看gcc如何使用vpackuswb ymm7, ymm5, ymm6
/vpermq ymm8, ymm7, 0xD8
自动矢量化标量基准版本。
v
= load+shift -> __m256i packed = _mm512_cvtepi16_epi8(v)
可能效果很好,即使它仅进行256b存储。
pshufb
对于一个寄存器来说是很好的,但在循环整个图像时,洗牌端口吞吐量会成为瓶颈。因此,您应该将高半部分与低半部分进行 AND 运算或向下移位以丢弃低半部分,然后_mm_packus_epi16
每对输入向量合并为一个输出向量。这里可能有重复的内容... - Peter Cordes_mm_and_si128(v,_mm_set1_epi16(0x00ff))
或_mm_srli_epi16(v,8)
? - Peter Cordes