实现变量XOR操作C++中内嵌汇编程序的正确方法

实现变量XOR操作C++中内嵌汇编程序的正确方法,c++,assembly,inline,xor,C++,Assembly,Inline,Xor,我最近看到一篇文章,介绍了如何使用xor’ing而不是使用临时变量来执行交换操作。当我使用int a^=b编译代码时;结果不会简单地出现在at&t语法之前 xor b, a etc. 相反,它会将原始值加载到寄存器中,对其进行异或运算并将其写回。 为了优化这一点,我想在内联汇编中编写它,这样它只使用三个记号来完成整个任务,而不是像通常那样使用15个记号 我尝试了多个关键字,如: asm(...); asm("..."); asm{...}; asm{"..."}; asm ... __asm

我最近看到一篇文章,介绍了如何使用xor’ing而不是使用临时变量来执行交换操作。当我使用int a^=b编译代码时;结果不会简单地出现在at&t语法之前

xor b, a
etc.
相反,它会将原始值加载到寄存器中,对其进行异或运算并将其写回。 为了优化这一点,我想在内联汇编中编写它,这样它只使用三个记号来完成整个任务,而不是像通常那样使用15个记号

我尝试了多个关键字,如:

asm(...);
asm("...");
asm{...};
asm{"..."};
asm ...
__asm ...
这些都不起作用,要么给我一个语法错误,gcc似乎不接受所有的语法,要么说

main.cpp: Assembler messages:
main.cpp:12: Error: too many memory references for `xor'

基本上,我想使用C++代码中定义的变量,在汇编块中使用三行XOR,然后让我的交换变量基本上像这样:

int main() {
    volatile int a = 5;
    volatile int b = 6;
    asm {
        xor a,b
        xor b,a
        xor a,b
    };
    //a should now be 6, b should be 5
}
澄清:
我希望避免使用编译器生成的mov操作,因为它们需要更多的cpu周期,而不仅仅是执行三个需要三个周期的xor操作。如何实现这一点?

要使用内联汇编,您应该使用。然而,这种类型的优化可能还为时过早。仅仅因为有更多的指令并不意味着代码更慢——有些指令可能真的很慢。例如,浮点BCD存储指令fbstp虽然被公认为罕见,但它需要超过200个周期——相比之下,简单的mov Agner Fog的一个周期是这些计时的良好资源

这样,我实现了一组交换函数,一些C++和一些汇编,并进行了一次测量,一次运行每个函数1亿次。 测试用例 交换 std::swap可能是这里的首选解决方案。它实现了您想要的交换两个变量的值的功能,适用于大多数标准库类型,而不仅仅适用于整数,清楚地传达了您想要实现的内容,并且可以跨架构移植

void std_swap(int *a, int *b) {
    std::swap(*a, *b);
}
这是生成的程序集:它将两个值加载到寄存器中,然后将它们写回相反的内存位置

movl    (%rdi), %eax
movl    (%rsi), %edx
movl    %edx, (%rdi)
movl    %eax, (%rsi)
异或交换 这就是您在C++中尝试做的:

void xor_swap(int *a, int *b) {
    *a ^= *b;
    *b ^= *a;
    *a ^= *b;
}
这不会直接转换为仅xor指令,因为x86上没有允许您直接对内存中的两个位置进行xor的指令-您始终需要将两个位置中的至少一个加载到寄存器中:

movl    (%rdi), %eax
xorl    (%rsi), %eax
movl    %eax, (%rdi)
xorl    (%rsi), %eax
movl    %eax, (%rsi)
xorl    %eax, (%rdi)
您还可以生成一组额外的指令,因为这两个指针可能是别名,即指向重叠的内存区域。然后,更改一个变量也会更改另一个变量,因此编译器需要不断存储和重新加载值。由于@Ped7g在注释中指出了此缺陷,使用编译器特定的_restrict关键字的实现将编译为与std_swap相同的代码

使用临时变量交换 这是带有临时变量的标准交换,编译器会立即将其优化为与std::swap相同的代码:

xchg指令 xchg可以将内存值与寄存器值进行交换——对于您的用例来说,它一开始似乎是完美的。但是,当您使用它访问内存时,它的速度非常慢,稍后您将看到这一点

void xchg_asm_swap(int *a, int *b) {
    __asm__ volatile (
        "movl    (%0), %%eax\n\t"
        "xchgl   (%1), %%eax\n\t"
        "movl    %%eax, (%0)"
        : "+r" (a), "+r" (b)
        : /* No separate inputs */
        : "%eax"
    );
}
我们需要将两个值中的一个加载到寄存器中,因为两个内存位置没有xchg

movl    (%rdi), %eax
movl    (%rsi), %edx
movl    %edx, (%rdi)
movl    %eax, (%rsi)
汇编中的异或交换 我在汇编中制作了两个版本的基于XOR的交换。第一个只加载寄存器中的一个值,第二个在交换和写回它们之前加载这两个值

void xor_asm_swap(int *a, int *b) {
    __asm__ volatile (
        "movl   (%0), %%eax\n\t"
        "xorl   (%1), %%eax\n\t"
        "xorl   %%eax, (%1)\n\t"
        "xorl   (%1), %%eax\n\t"
        "movl   %%eax, (%0)"
        : "+r" (a), "+r" (b)
        : /* No separate inputs */
        : "%eax"
    );
}

void xor_asm_register_swap(int *a, int *b) {
    __asm__ volatile (
        "movl   (%0), %%eax\n\t"
        "movl   (%1), %%ecx\n\t"
        "xorl   %%ecx, %%eax\n\t"
        "xorl   %%eax, %%ecx\n\t"
        "xorl   %%ecx, %%eax\n\t"
        "movl   %%eax, (%0)\n\t"
        "movl   %%ecx, (%1)"
        : "+r" (a), "+r" (b)
        : /* No separate inputs */
        : "%eax", "%ecx"
    );
}
结果 您可以在上查看完整的编译结果以及生成的汇编代码

在我的机器上,以微秒为单位的计时略有不同,但通常具有可比性:

std_swap:              127371
xor_swap:              150152
tmp_swap:              125896
xchg_asm_swap:         699355
xor_asm_swap:          130586
xor_asm_register_swap: 124718
您可以看到std_交换、tmp_交换、xor_asm_交换和xor_asm_寄存器交换通常在速度上非常相似-事实上,如果我将xor_asm_寄存器交换移到前面,它会比std_交换稍微慢一点。还要注意的是,tmp_交换与std_交换完全相同,尽管它的测量速度通常要快一点,可能是因为排序的缘故

在C++中实现的P> XORISWAP稍微慢一些,因为编译器因为别名而生成了每个指令的附加内存加载/存储,如我们修改XORIX交换来取int *yxLimuleA,int **限制B,意味着A和B从不别名,编译器生成与std_交换和tmp_交换相同的代码

尽管使用的指令数量最少,但xchg_交换速度非常慢,是其他任何选项的四倍,这只是因为如果涉及内存访问,xchg不是一个快速操作

最终,您可以选择使用一些难以理解和维护的自定义基于程序集的版本,或者只使用std::swap,这与标准库设计人员提出的任何优化都是完全相反的,并且还可以从中获益,例如,对更大的类型使用矢量化。因为这是超过一亿次的迭代, 应该清楚的是,在这里使用汇编代码的潜在改进是非常小的——如果您有任何改进,而这些改进并不清楚,那么您最多可以节省几微秒

TL;医生:你不应该那样做,只要用std::swapa,b

附录:\ asm\挥发性 我认为在这一点上解释一下内联汇编代码可能是有意义的__在GNU模式下,asm足够引入一块汇编代码。volatile用于确保编译器不会将其优化掉——否则它只会删除块

有两种形式的“asm”volatile。其中一个还涉及goto标签;我在这里不谈这个问题。另一种形式最多有四个参数,用冒号分隔:

最简单的一种形式是:yasasm挥发rdtSC,它只是卸载汇编代码,但实际上并不与它周围的C++代码交互。特别是,您需要猜测变量是如何分配给寄存器的,这并不是很好。 请注意,汇编代码指令以\n分隔,因为此汇编代码是逐字传递给GNU汇编程序gas的。 第二个参数是输出操作数的列表。您可以指定它们的具体类型,=r表示任何寄存器操作数,+r表示任何寄存器操作数,但它也用作输入。例如,:+RA,+RB告诉编译器用包含a的寄存器替换%0引用的第一个操作数,用包含b的寄存器替换%1。 此符号表示您需要将%eax替换为%%eax,因为您通常会将AT&T汇编符号中的eax引用为%%eax以转义百分号。 如果愿意,还可以使用.intel\u语法\n切换到英特尔的汇编语法。 第三个参数相同,但只处理输入操作数。 第四个参数告诉编译器哪些寄存器和内存位置会丢失它们的值,以便围绕汇编代码进行优化。例如,关闭内存可能会提示编译器插入一个完整的内存围栏。您可以看到,我已将用于临时存储的所有寄存器添加到此列表中。
为什么要在组装中这样做?a^b;b^a;a^b;我也会这么做。说真的,只需使用std::swapa,b;不要试图智取编译器。许多体系结构都有专用的寄存器,否则复制到临时寄存器通常要快得多。在代码中,volatile强制每行的操作数和每个操作的结果从内存读/写到内存。为什么在你的例子中a和b是易变的。。。问题是,gcc确实认识到它是交换操作,并且它使用mov指令和临时寄存器以更快的变体在机器代码中重写它,它将拒绝发出不太理想的xor变体。因此,这只是演示如何让-O3真正做一些合理的事情,而不是删除所有代码。如果你写错了C++,就没有必要通过内联汇编来优化它,这是最后一步,而不是第一步。如果你想玩弄汇编,就像你实际上想要慢xor交换一样,那么就使用独立汇编。@supremeGod 1 asm指令与1 CPU周期不同。有些指令需要数十、数百或更多的周期才能完成。有些只吃几片。你不能通过仅仅计算指令的数量来衡量效率。注意,如果XCHG使用它来交换两个寄存器中的值,那么实际上是相当快的。注意,C++ XORSWAP源不向编译器发出两个指针都不别名的信号,这将允许编译器完全避免xor指令,并使用交换代码的mov唯一变体。。。尝试void xor_swapint*__restricta,int*b,看看它是如何折叠回类似std::swap的代码的,这会给程序员带来压力,让他们永远不要使用别名相同的内存调用它。。。实际上,在交换的情况下,即使使用别名内存调用它也会产生正确的结果,因为相同的值被交换为相同的值,但通常OP假设两个不同的变量。您的答案在显示xor变量时非常有用。但是如果一个人试图公平,并使用完全C++的专门知识,那么OP假定两个不同的内存变量,因此公平地让编译器知道,然后编译器显示出对机器的更好的理解,并且完全避免了较慢的XOR变体。编写正确的C++是非常棘手的,编译器给程序员编写汇编变量时使用的相同假设。不幸的是,编写一些不理想的C++并显示更好的汇编和编译器是很容易的,我也做了很多次。上帝:asm在这里是没有意义的。这是输入的纯函数,如果输出操作数未使用,您希望让编译器对其进行优化。或CSE相同输入的多个交换。当然,这就是为什么你不希望内联asm在第一位;编译器在使用std::swap时已经做得更好了。实际上,这个版本中的volatile可能是部分的,但是 对于其中的bug,解决方法不足:在寄存器中向内联asm传递指针并不意味着内联asm读取指向内存的指针。在没有内存阻塞的情况下,gcc/clang将优化掉指向内存中明显死掉的存储,或者至少使用asm对它们重新排序。这就是为什么应该使用+m*a读/写内存操作数。看到和