C语言中交换两个相同大小的内存块的最快方法是什么?

14
什么是交换两个大小相等、不重叠的内存区域的最快方法?比如说,我需要交换(t_Some *a)(t_Some *b)。考虑到时间与空间的权衡,增加临时空间能提高速度吗?例如,(char *tmp)(int *tmp)哪个更好?我正在寻找一个可移植的解决方案。
原型:
void swap_elements_of_array(void* base, size_t size_of_element, int a, int b);

便携解决方案 - 看起来你没有太多选择... - valdo
6
请确认一下:您是否确实需要这些指针保持它们的值,交换指针不会起作用吗? - salezica
试着找出一种只交换指针而不交换它们所指向的数据的方法。 - nos
2
你的原型中只有一个指针。ab是要交换的元素的索引吗? - Didier Trosset
@David Heffernan:他将不得不将东西复制到一个临时位置,并询问该临时位置应该有多大。 - sharptooth
9个回答

7
移动内存块最快的方法是使用中的memcpy()。如果你从a复制到temp,从b复制到a,然后从temp复制到b,你将使用优化的库例程进行交换,编译器可能会内联。你不想一次性复制整个块,而是以向量大小的块进行复制。
实际上,如果你编写一个紧凑的循环,编译器可能会发现你正在交换数组的每个元素并进行相应的优化。在大多数现代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;
  }
}

如果你按照字面意思将其转换为机器码,那将是一个糟糕的算法,每次只复制一个字节,在每次迭代中执行两次递增等操作。实际上,编译器会看到你真正想做什么。
在clang 5.0.1中使用-std=c11 -O3,它将生成以下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

说服编译器一次只处理一个单词的指令,而不是向量化循环,与您想要的相反!

5

您最好的选择是最大化寄存器使用,这样当您读取临时变量时,您不会得到额外的(可能被缓存的)内存访问。寄存器数量将取决于系统,而寄存器分配(将变量映射到实际寄存器的逻辑)将取决于编译器。因此,您最好的选择是只期望一个寄存器,并期望其大小与指针相同。这归结为处理解释为size_t数组块的简单for循环。


1
除非两个块具有不同的对齐方式,否则循环并不那么简单,因为您无法将其可移植地解释为 size_t[] - Steve Jessop

2
如果两个内存区域很大并且适合整数个内存页面,则可以交换它们的页表项以交换它们的内容,而不使用memcpy()或XORs。
理论上,对于两个2MiB大的页面,您只需要写入16个字节的分页结构即可交换它们在虚拟地址空间中的映射...因此也交换了它们的内容。
在x86-64 CPU的64位模式下,1GiB页面是可能的,并且可以通过写入几个字节的分页结构来交换2个这样的1GiB内存块的内容。
这种方法的警告是访问分页结构需要内核模式特权或使用用户模式的共享内存映射函数。
随着最近Meltdown补丁(KPTI)的推出,从用户模式转换到内核模式变得更加昂贵。也许太昂贵了,无法使4kiB内存页面交换与memcpy()竞争...但如果您有要交换的2MB或更大内存块,则交换它们的分页结构更快。

这个解决方案似乎与可移植性相反,OP没有标记操作系统,只有C。 - Evan Benn
1
是的,这个解决方案不是非常具有可移植性,但也不是完全不能用,因为它将在任何带有内存分页单元的CPU上运行。这意味着任何大型英特尔或AMD CPU以及一些ARM CPU都可以使用,包括大多数服务器、台式机和移动CPU。但不包括微控制器... - George Robinson
你已经让我相信这个概念至少是相当可移植的。但是在不同的操作系统、编译器和芯片组合之间,实现是否真的是相同的C代码呢? - Evan Benn
1
从内核模式来看,在任何x86 Intel/AMD CPU上交换两个页面的代码都是相同的,无论操作系统如何,但获取这两个页面地址的代码可能是特定于操作系统的。从用户模式来看,所有的代码都是特定于操作系统的,因为你必须依赖内核暴露的内存映射函数。对于运行在ARM CPU上的操作系统也是如此。 - George Robinson

2

Word writes(单词写入)将是最快的。但是,需要考虑块大小和对齐方式。在实践中,通常会合理对齐,但不应该依赖于它。 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);
}

0

这个速度在一定程度上取决于平台,只有通过测试才能真正得出结论。

个人倾向于创建一个与数组大小相等的内存块;使用memcpy交换内容,使用新创建的内存块作为交换空间。

现在,内存块的大小将影响操作的速度(同样取决于平台),因此您可能会发现,对于非常大的数组,来回交换较小量的数据比每次交换大块更快。

编辑

鉴于评论,请让我解释一下,关于交换较小量数据的最后一条评论。

您的目标是使用临时交换空间tmpa数据传输到b,并将b数据传输到a

tmp的大小等于或小于ab的大小,并且随着tmp的减少,交换数据的迭代次数增加,例如,如果tmpa的十分之一,则需要10次迭代。

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


你的意思是将整个 a 复制到 tmp,然后将整个 b 复制到 a,最后将整个 tmp 复制到 a?从缓存的角度来看,这样做效率不高。 - Oliver Charlesworth
我会在我的回答中澄清我的意思。 - ChrisBD

0

显然,您必须将A复制到Temp,将B复制到A,然后将Temp复制到B。您可以一次性完成此操作,用于小范围,或者对于大范围,可以分段执行此操作,其中您不希望分配如此大的Temp值。选择段大小取决于您,尽管考虑适合硬件的对齐和缓存问题,对于大型、频繁的移动非常重要。

(实际上还有另一种方法,不需要任何临时空间:将A与B异或,然后将B与A异或,最后将A与B异或。这是老一辈汇编程序员的技巧。)


0

您可以使用此处描述的逻辑。这样,您可以节省第三个缓冲区。

#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;
    }
}

乍一看,它们两个都因为访问了很多数组(在第一种情况下)和每次循环只处理一个字节而显得昂贵,但是如果你让编译器优化这个问题,那应该没问题,因为(至少gcc)足够聪明,可以将4个步骤(在x64中:甚至16个步骤)捆绑成一个循环运行。

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


2
-1:这将调用未定义的行为。而且XOR-swap技巧可能会阻止编译器优化。 - Oliver Charlesworth
  1. 正如我所说的,至少gcc能够识别并优化所尝试做的事情。
  2. 你能更具体地说明一下UB吗?
- glglgl
1
缺乏序列点。请查看Wikipedia文章 - Oliver Charlesworth
你是完全正确的。将示例更改为a) 正确的XOR算法和b) “正常”的交换算法,因为第一个算法不能实现变量的保存。 - glglgl

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;
}

上述片段的意图是允许高度优化的libc版本的memcpy(或编译器内联)拥有所需的所有自由度。对齐非常关键。如果VLAs不可用(在C99之前),可以使用一个时髦的do-while组成宏。

如果size_of_element很大,从缓存的角度来看,这看起来并不是很高效,除非编译器足够聪明以交错执行memcpy - Oliver Charlesworth
C99风格不太具有高度可移植性。你确定memcpy比循环:swp(i32,i32)更快,是因为临时内存(而非寄存器)吗? - psihodelia
1
足够好的工作。在没有汇编专家的情况下,很难超过libc的智能。我同意对于较大的尺寸,一个“外部”内循环(缓存大小,以缓存边界对齐)可能会更好。 - wildplasser
1
@psilodelia:这就是我说的:将上面的代码宏化是微不足道的。(留作练习) - wildplasser

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接