2

我有任意大小的图像缓冲区,我以 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;
        }
    }
}

它运行得很快,但我很好奇是否可以做任何事情来改进它并获得额外的几毫秒。如果它涉及到汇编代码,我宁愿避免这种情况,但我愿意添加额外的库。

4

2 回答 2

2

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

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

我找到了这篇文档——Optimizing CPU to Memory Accesses on the SGI Visual Workstations 320 and 540——这似乎是上述代码的灵感来源,但适用于老一代处理器;但是,它确实包含大量关于其工作原理的讨论。

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

Pentium II 和 III CPU 高速缓存在 32 字节高速缓存行大小的块上运行。当数据被写入或从(缓存的)内存中读取时,整个缓存行都会被读取或写入。虽然这通常会提高 CPU 内存性能,但在某些情况下可能会导致不必要的数据获取。特别是,考虑 CPU 将执行 8 字节 MMX 寄存器存储的情况: movq. 由于这只是缓存行的四分之一,因此从缓存的角度来看,它将被视为读-修改-写操作;目标缓存行将被提取到缓存中,然后将发生 8 字节写入。在内存副本的情况下,获取的数据是不必要的;随后的存储将覆盖缓存行的其余部分。可以通过让 CPU 将所有写入收集到高速缓存行然后对内存进行单次写入来避免读取-修改-写入行为。将单个写入合并到单个高速缓存行写入称为写入组合。当正在写入的内存被显式标记为写组合(相对于缓存或非缓存)或使用 MMX 非临时存储指令时,就会发生写组合。内存一般只有在帧缓冲区使用时才标记为写组合;分配的内存VirtualAlloc要么是未缓存的,要么是缓存的(但不是写组合)。MMXmovntpsmovntq 非临时存储指令指示 CPU 将数据直接写入内存,绕过 L1 和 L2 缓存。作为副作用,如果目标内存被缓存,它还可以启用写入组合。

如果您更喜欢使用 memcpy,请考虑调查您正在使用的 memcpy 实现的源代码。一些 memcpy 实现会寻找本地字对齐缓冲区,以通过使用完整的寄存器大小来提高性能;其他人会使用 native-word-aligned 自动复制尽可能多的内容,然后清理剩余部分。确保您的缓冲区是 8 字节对齐的将有助于这些机制。

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

于 2015-04-30T16:30:34.723 回答
1

你的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 实现应该已经完成​​了这些事情。

于 2015-04-30T15:52:40.237 回答