将每秒字节快速复制到新的存储区域

akw*_*akw 4 c performance sse memcpy sse2

我需要一种快速的方法将每个第二个字节复制到一个新的malloc内存区域.我有一个RGB数据和每通道16位(48位)的原始图像,并希望创建一个每通道8位(24位)的RGB图像.

有没有比按字节复制更快的方法?我对SSE2了解不多,但我想SSE/SSE2是可能的.

Pet*_*des 6

您的RGB数据已打包,因此我们实际上不必关心像素边界.问题是只是打包一个数组的每个其他字节.(至少在图像的每一行内;如果使用16或32B的行步长,则填充可能不是整数像素.)

这可以使用SSE2,AVX或AVX2 shuffle有效地完成.(还有AVX512BW,甚至AVX512VBMI可能更多,但第一个AVX512VBMI CPU可能不会有一个非常有效vpermt2b的2输入通道字节混洗.)


您可以使用SSSE3 pshufb来获取所需的字节,但它只是一个1输入的shuffle,它将为您提供8个字节的输出.一次存储8个字节比一次存储16个字节需要更多的存储指令.(自Haswell以来,您还会遇到Intel CPU上的随机吞吐量瓶颈问题,Haswell只有一个shuffle端口,因此每个时钟有一个随机播放吞吐量).(您也可以考虑使用2x pshufb+ por来提供16B存储,这对Ryzen来说可能很好.使用2个不同的shuffle控制向量,一个将结果置于低64b,另一个将结果置于高64b.请参阅转换8个16位SSE寄存器到8位数据).

相反,使用_mm_packus_epi16(packuswb)可能是一个胜利.但是由于它不会丢弃而不是丢弃你不想要的字节,所以你必须用你希望保存在每个16位元素的低字节中的数据来输入它.

在您的情况下,这可能是每个RGB16组件的高字节,丢弃每个颜色组件的8个最低有效位.即 _mm_srli_epi16(v, 8). 要将每个16位元素中的高字节归零,请_mm_and_si128(v, _mm_set1_epi16(0x00ff))改为使用.(在这种情况下,不要忘记使用未对齐的负载来替换其中一个班次的所有东西;这是简单的情况,你应该只使用两个AND来提供PACKUS.)

这或多或少是gcc和clang如何自动矢量化这个,at -O3.除非他们都搞砸了,浪费显著指令(https://gcc.gnu.org/bugzilla/show_bug.cgi?id=82356,https://bugs.llvm.org/show_bug.cgi?id=34773).尽管如此,让他们使用SSE2(x86-64的基线)或者使用NEON for ARM或其他任何东西进行自动矢量化是一种很好的安全方式,可以获得一些性能而不会在手动矢量化时引入错误.在编译器错误之外,它们生成的任何内容都将正确实现此代码的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);
}
Run Code Online (Sandbox Code Playgroud)

请参阅Godbolt上的此代码和更高版本的代码+ asm.

// 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);
}
Run Code Online (Sandbox Code Playgroud)

但在许多微架构中,矢量移位吞吐量限制为每时钟1个(英特尔在Skylake之前,AMD Bulldozer/Ryzen).此外,在AVX512之前没有加载+移位asm指令,因此很难通过管道获得所有这些操作.(即我们很容易在前端出现瓶颈.)

我们可以从一个偏移了一个字节的地址加载,而不是移位,这样我们想要的字节就在正确的位置.并屏蔽我们想要的字节具有良好的吞吐量,特别是对于AVX,编译器可以将负载+折叠成一条指令.如果输入是32字节对齐的,并且我们只对奇数向量执行此偏移加载技巧,则我们的加载将永远不会跨越缓存行边界.使用循环展开,这可能是许多CPU上SSE2或AVX(没有AVX2)的最佳选择.

// 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);
}
Run Code Online (Sandbox Code Playgroud)

如果没有AVX,内部循环每16B结果矢量需要6条指令(6个uop).(对于AVX,它只有5,因为负载折叠到和).由于前端的完全瓶颈,循环展开有很大帮助. gcc -O3 -funroll-loops这个手动矢量化版本看起来很不错,尤其是gcc -O3 -funroll-loops -march=sandybridge启用AVX.

随着AVX,它可能是值得做的事情都v0v1and,以减少前端瓶颈具有高速缓存行分裂的成本.(偶尔会有页面拆分).但也许不是,取决于uarch,以及你的数据是否已经错位.(对此进行分支可能是值得的,因为如果L1D中的数据很热,则需要最大化缓存带宽).

对于AVX2,具有256b负载的256b版本应该在Haswell/Skylake上运行良好.对于src64B对齐,偏移负载仍将永远不会缓存行拆分.(它将始终加载[62:31]缓存行的字节,并且v0加载将始终加载字节[31:0]).但是在128b通道内打包工作,所以在打包之后你必须将(以vpermq)按顺序放入64位块.看看gcc如何使用vpackuswb ymm7, ymm5, ymm6/ 自动向量化标量基线版本vpermq ymm8, ymm7, 0xD8.

使用AVX512F,此技巧将停止工作,因为64B负载必须对齐以保持在单个64B高速缓存行内.但是对于AVX512,可以使用不同的shuffle,并且ALU uop吞吐量更加珍贵(在Skylake-AVX512上,其中port1关闭而512b uops在飞行中).所以v= load + shift - > __m256i packed = _mm512_cvtepi16_epi8(v)可能效果很好,即使它只有256b存储.

正确的选择可能取决于你的src和dst通常是64B对齐的.KNL没有AVX512BW,所以这可能只适用于Skylake-AVX512.