在amd64体系结构上的C++中,将图像缓冲区blit到另一个缓冲区的xy偏移中的最快方法



我有任意大小的图像缓冲区,以x,y偏移量复制到大小相等或更大的缓冲区中。颜色空间为BGRA。我目前的复制方法是:

void render(guint8* src, guint8* dest, uint src_width, uint src_height, uint dest_x, uint dest_y, uint dest_buffer_width) {
bool use_single_memcpy = (dest_x == 0) && (dest_y == 0) && (dest_buffer_width == src_width);
if(use_single_memcpy) {
memcpy(dest, src, src_width * src_height * 4);
}
else {
dest += (dest_y * dest_buffer_width * 4);
for(uint i=0;i < src_height;i++) {
memcpy(dest + (dest_x * 4), src, src_width * 4);
dest += dest_buffer_width * 4;
src += src_width * 4;
}
}
}

它跑得很快,但我很好奇我是否可以做些什么来改进它,并增加几毫秒的时间。如果它涉及到汇编代码,我宁愿避免这样做,但我愿意添加额外的库。

StackOverflow上一个流行的答案是,它确实使用x86-64汇编和SSE,可以在这里找到:用于图像处理的非常快的memcpy?。如果您确实使用此代码,则需要确保缓冲区是128位对齐的。对该代码的基本解释是:

  • 使用非临时存储,因此可以绕过不必要的缓存写入,并可以组合对主内存的写入
  • 读取和写入只交错在非常大的块中(先进行多次读取,然后进行多次写入)。背靠背执行多次读取通常比单次读取-写入-读取-写入模式具有更好的性能
  • 使用了更大的寄存器(128位SSE寄存器)
  • 预取指令包含在CPU流水线操作的提示中

我发现了这篇文档-在SGI Visual Workstations 320和540上优化CPU到内存访问-这似乎是上述代码的灵感来源,但适用于旧一代处理器;然而,它确实包含了大量关于它如何工作的讨论。

例如,考虑一下关于写组合/非临时存储的讨论:

奔腾II和III CPU缓存在32字节缓存行大小上运行块。当数据被写入或从(缓存的)内存中读取时,整个高速缓存行被读取或写入。虽然这通常会增强CPU内存性能,在某些情况下可能导致不必要的数据获取。特别是,考虑CPU将执行8字节MMX寄存器存储:movq。因为这只是一个缓存行的四分之一,它将被视为读-修改-写从缓存的角度进行操作;目标缓存行将是提取到缓存中,则将进行8字节写入。在内存复制,这种提取的数据是不必要的;后续门店将覆盖高速缓存行的剩余部分。读取修改写入通过让CPU收集对缓存的所有写入,可以避免这种行为行,然后对内存进行一次写入。合并个人写作写入到单个高速缓存行中被称为写入组合。当要写入的内存为显式标记为写组合(与缓存或未缓存),或者使用MMX非临时存储指令时。内存通常只有在帧缓冲器;VirtualAlloc分配的内存要么未缓存,要么缓存(但不进行写组合)。MMXmovntpsmovntq非临时存储指令指示CPU写入数据绕过L1和L2高速缓存直接到存储器。作为副作用,如果缓存了目标内存,它还可以进行写组合。

如果您更喜欢使用memcpy,请考虑研究您正在使用的memcpy实现的源代码。一些memcpy实现寻找本机字对齐缓冲区,通过使用完整的寄存器大小来提高性能;其他人会使用原生单词对齐自动复制尽可能多的单词,然后清除剩余的单词。确保缓冲区是8字节对齐的将有助于这些机制。

一些memcpy实现包含大量的前置条件,以使其对小缓冲区(<512)有效——您可能需要考虑将这些块剥离的代码复制粘贴,因为您可能不使用小缓冲区。

您的use_single_memcpy测试限制性太强。稍微重新排列一下就可以删除dest_y == 0要求。

void render(guint8* src, guint8* dest,
uint src_width, uint src_height, 
uint dest_x, uint dest_y,
uint dest_buffer_width)
{
bool use_single_memcpy = (dest_x == 0) && (dest_buffer_width == src_width);
dest_buffer_width <<= 2;
src_width <<= 2;
dest += (dest_y * dest_buffer_width);
if(use_single_memcpy) {
memcpy(dest, src, src_width * src_height);
}
else {
dest += (dest_x << 2);
while (src_height--) {
memcpy(dest, src, src_width);
dest += dest_buffer_width;
src += src_width;
}
}
}

我还将循环改为倒计时(这可能更有效),删除了一个无用的临时变量,并取消了重复计算。

使用SSE内部函数一次复制16个字节而不是4个字节可能会做得更好,但您需要担心对齐和4个像素的倍数。一个好的memcpy实现应该已经做了这些事情。

最新更新