C - 交换两个相同大小的内存块的最快方法?

时间:2011-11-17 11:41:02

标签: c caching memory swap

交换两个相同大小的非重叠内存区域的最快方法是什么?说,我需要将(t_Some *a)(t_Some *b)交换。考虑到时空权衡,会增加临时空间提高速度吗?例如,(char *tmp) vs (int *tmp)?我正在寻找便携式解决方案。

原型:

void swap_elements_of_array(void* base, size_t size_of_element, int a, int b);

9 个答案:

答案 0 :(得分:5)

移动内存块的最快方法是memcpy()来自<string.h>。如果您memcpy()atempmemmove()bamemcpy()来自tempb,您将拥有一个使用优化库例程的交换,编译器可能会在其中进行内联。您不希望一次复制整个块,而是使用矢量大小的块。

实际上,如果编写一个紧密循环,编译器可能会告诉您正在交换数组的每个元素并进行相应的优化。在大多数现代CPU上,您需要生成向量指令。如果确保所有三个缓冲区都已对齐,它可能会生成更快的代码。

但是,您真正想要做的是让优化器更容易。采取这个计划:

#include <stddef.h>

void swap_blocks_with_loop( void* const a, void* const b, const size_t n )
{
  unsigned char* p;
  unsigned char* q;
  unsigned char* const sentry = (unsigned char*)a + n;

  for ( p = a, q = b; p < sentry; ++p, ++q ) {
     const unsigned char t = *p;
     *p = *q;
     *q = t;
  }
}

如果你把它翻译成机器码,就像字面写的一样,这是一个糟糕的算法,一次复制一个字节,每次迭代做两次递增,依此类推。但实际上,编译器会看到你真正想要做的事情。

在使用-std=c11 -O3的clang 5.0.1中,它在x86_64上生成(部分)以下内部循环:

.LBB0_7:                                # =>This Inner Loop Header: Depth=1
        movups  (%rcx,%rax), %xmm0
        movups  16(%rcx,%rax), %xmm1
        movups  (%rdx,%rax), %xmm2
        movups  16(%rdx,%rax), %xmm3
        movups  %xmm2, (%rcx,%rax)
        movups  %xmm3, 16(%rcx,%rax)
        movups  %xmm0, (%rdx,%rax)
        movups  %xmm1, 16(%rdx,%rax)
        movups  32(%rcx,%rax), %xmm0
        movups  48(%rcx,%rax), %xmm1
        movups  32(%rdx,%rax), %xmm2
        movups  48(%rdx,%rax), %xmm3
        movups  %xmm2, 32(%rcx,%rax)
        movups  %xmm3, 48(%rcx,%rax)
        movups  %xmm0, 32(%rdx,%rax)
        movups  %xmm1, 48(%rdx,%rax)
        addq    $64, %rax
        addq    $2, %rsi
        jne     .LBB0_7

而具有相同标志的gcc 7.2.0也会进行矢量化,而不是展开循环:

.L7:
        movdqa  (%rcx,%rax), %xmm0
        addq    $1, %r9
        movdqu  (%rdx,%rax), %xmm1
        movaps  %xmm1, (%rcx,%rax)
        movups  %xmm0, (%rdx,%rax)
        addq    $16, %rax
        cmpq    %r9, %rbx
        ja      .L7

说服编译器生成一次只能处理单个单词的指令,而不是对循环进行矢量化,这与你想要的相反!

答案 1 :(得分:4)

您最好的选择是最大限度地提高寄存器的使用率,这样当您读取临时文件时,您最终不会获得额外的(可能是缓存的)内存访问。寄存器数量取决于系统和寄存器分配(将变量映射到实际寄存器的逻辑)将取决于编译器。所以你最好的选择是我希望只有一个寄存器,并期望它的大小与指针相同。这归结为一个简单的for循环处理解释为size_t数组的块。

答案 2 :(得分:2)

Word写入速度最快。但是,需要考虑块大小和对齐。在实践中,事情通常是合理的,但你不应该指望它。 memcpy()可以安全地处理所有内容,并且可以在合理范围内针对不变大小进行专门化(内置)。

这是一种便携式解决方案,在大多数情况下运行良好

static void swap_byte(void* a, void* b, size_t count)
{
    char* x = (char*) a;
    char* y = (char*) b;

    while (count--) {
        char t = *x; *x = *y; *y = t;
        x += 1;
        y += 1;
    }
}

static void swap_word(void* a, void* b, size_t count)
{
    char* x = (char*) a;
    char* y = (char*) b;
    long t[1];

    while (count--) {
        memcpy(t, x, sizeof(long));
        memcpy(x, y, sizeof(long));
        memcpy(y, t, sizeof(long));
        x += sizeof(long);
        y += sizeof(long);
    }
}

void memswap(void* a, void* b, size_t size)
{
    size_t words = size / sizeof(long);
    size_t bytes = size % sizeof(long);
    swap_word(a, b, words);
    a = (char*) a + words * sizeof(long);
    b = (char*) b + words * sizeof(long);
    swap_byte(a, b, bytes);
}

答案 3 :(得分:1)

如果2个内存区域很大并且适合整数个内存页面,那么您可以交换它们的页表条目,以便在不使用memcpy()或XOR的情况下交换它们的内容。

理论上,对于两个大的2MiB页面,您只需要编写16个字节的分页结构来交换它们在虚拟地址空间中的映射......以及它们的内容。

在64位模式的x86-64 CPU上可以使用1GiB页面,并且只能写入几个字节的分页结构,也可以交换2个这样的1GiB内存块的内容。

此方法的警告是,对分页结构的访问需要内核模式权限或使用用户模式下的共享内存映射功能。

使用最近的Meltdown补丁(KPTI),从用户模式转换到内核模式变得更加昂贵。使4kiB内存页面swapps与memcpy()竞争可能太昂贵......但是如果你有2MB或更大的内存块可以交换,那么交换它们的Paging Structures会更快。

答案 4 :(得分:0)

此速度将部分取决于平台,并且只有通过测试才能真正证实。

我个人赞成创建一个与其中一个数组大小相同的内存块;使用memcpy交换周围的内容,使用新创建的内存块作为交换空间。

现在内存块的大小会对操作速度产生影响(再次取决于平台),因此您可能会发现,对于非常大的阵列来说,来回交换少量数据比交换大块数据更快时间。

修改

根据评论,让我解释一下,我关于交换少量数据的最后评论。

您的目标是使用临时交换空间ab数据转移到ba数据到tmp

tmp的大小等于或小于ab的大小,并且交换数据的迭代次数会随着tmp的大小而增加例如减少如果tmpa的第10个,则需要进行10次迭代。

现在为了帮助memcpy的速度,最好确保为数组(a,b和tmp)分配对齐的内存空间。

答案 5 :(得分:0)

#include <string.h>
#include <stdio.h>

static void swap_elements_of_array(void* base, size_t size_of_element, int a, int b);
static void swap_elements_of_array(void* base, size_t size_of_element, int a, int b)
{
union {
    int i; /* force alignment */
    char zzz[size_of_element] ; /* VLA */
    } swap;
memcpy (swap.zzz, (char*)base + a * size_of_element,size_of_element);
memcpy ((char*)base + a * size_of_element,(char*)base + b * size_of_element,size_of_element);
memcpy ((char*)base + b * size_of_element, swap.zzz, size_of_element);
}

int main (void)
{
unsigned idx,array[] = {0,1,2,3,4,5,6,7,8,9};

swap_elements_of_array(array, sizeof array[0], 2, 5);

for (idx=0; idx < 10; idx++) {
    printf( "%u%c", array[idx], (idx==9) ? '\n' : ' ' );
    }
return 0;
}

上述片段的目的是允许高度优化的memcpy libc版本(或编译器内联)获得所需的所有自由。对齐至关重要。如果VGA不可用(在C99之前),可以使用时髦的do-while组成宏。

答案 6 :(得分:0)

我想分享一下我多年来一直在没有戏剧性的微控制器上使用过的简单解决方案。

#define swap(type, x, y) { type _tmp; _tmp = x; x = y; y = _tmp; }

好的...它会创建一个堆栈变量,但通常用于uint8_t,uint32_t,float,double等。但是它也应该在结构上起作用。

编译器应该足够聪明,以便在类型大小允许的情况下可以将堆栈变量替换为寄存器。

真的只适用于小型类型……这可能适合99%的情况。

也可以使用“ auto”代替传递类型...但是我想更灵活一些,我想可以将“ auto”作为传递类型。

示例...

swap(uint8_t, var1, var2) 
swap(float, fv1, fv2)
swap(uint32_t, *p1, *p2) // will swap the contents as p1 and p2 are pointers
swap(auto, var1, var2) // should work fine as long as var1 and var2 are same type

答案 7 :(得分:-1)

您可以使用here描述的逻辑。这样,您可以保存第三个缓冲区。

#include <stddef.h>
#include <stdint.h>
void swap(uint8_t *a, uint8_t *b, size_t length) {
    size_t i;
    for (i=0; i<length; i++) {
        uint8_t aa = a[i];
        aa^=b[i];
        b[i]^=aa;
        aa^=b[i];
        a[i] = aa;
    }
}

即使只有这一个临时变量也足以帮助编译器优化它。


但是如果使用这样的临时变量,你也可以这样做

#include <stddef.h>
#include <stdint.h>
void swap(uint8_t *a, uint8_t *b, size_t length) {
    size_t i;
    for (i=0; i<length; i++) {
        uint8_t aa = a[i];
        a[i] = b[i];
        b[i] = aa;
    }
}

乍一看,由于许多数组访问(在第一种情况下)和每次循环运行只处理一个字节,它们都看起来很昂贵,但是如果你让你的编译器优化它,它应该没问题, as(至少gcc)非常聪明,可以将4个步骤(x64:甚至16个步骤)捆绑到一个循环运行中。

请注意,您的编译器可能不会如此积极地进行优化,因此您可能必须自己进行上述拆分。在这种情况下,请注意对齐。

答案 8 :(得分:-1)

显然,您必须将A复制到Temp,将B复制到A,然后将Temp复制到B.您可以一次性完成所有操作,对于较小的区域,或者在较大区域的部分执行此操作我不想分配如此大的Temp值。部分大小的选择取决于您,但考虑到适合硬件的对齐和缓存问题对于大型,频繁的移动非常重要。

(嗯,实际上还有另外一种方法,它不需要任何临时空间:XOR A与B,然后XOR B与A,然后XOR A与B。旧的汇编程序员的伎俩。)