在C ++中实现对变量的Xor操作的内联汇编程序的正确方法

时间:2019-02-17 12:59:23

标签: c++ assembly inline xor

我最近看过一篇关于如何使用异或运算而不是使用临时变量来执行交换操作的文章。当我使用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 ++代码中定义的变量,使用三行对其进行异或,然后使交换的变量基本上像这样:

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操作要花费三个周期。我该怎么办?

1 个答案:

答案 0 :(得分:6)

要使用内联汇编,应使用__asm__ volatile。但是,这种类型的优化可能为时过早。仅仅因为有更多的指令并不意味着代码会变慢-有些指令可能会真正变慢。例如,浮点BCD存储指令(fbstp)虽然很少见,但需要200多个周期-与简单的mov的一个周期相比(Agner Fog的Optimization Guide是一个很好的资源这些时间)。

因此,我实现了一堆“交换”功能,其中一些使用C ++,某些使用汇编语言,并做了一些测量,使每个功能连续运行一亿次。


测试用例

std::swap

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)

XOR交换

这是您要使用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)

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

交换临时变量

这是带有临时变量的“标准”交换(编译器立即将其优化为与std::swap相同的代码):

void tmp_swap(int *a, int *b) {
    int tmp = *a;
    *a = *b;
    *b = tmp;
}

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

程序集中的XOR交换

我在Assembly中制作了两个版本的基于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"
    );
}

结果

您可以在Godbolt上查看完整的编译结果以及生成的汇编代码。

在我的机器上,时间(以微秒为单位)略有不同,但通常是可比较的:

std_swap:              127371
xor_swap:              150152
tmp_swap:              125896
xchg_asm_swap:         699355
xor_asm_swap:          130586
xor_asm_register_swap: 124718

您会发现std_swaptmp_swapxor_asm_swapxor_asm_register_swap的速度通常非常相似-实际上,如果我将xor_asm_register_swap移至前面的结果比std_swap慢一点。还请注意,tmp_swapstd_swap完全相同,但汇编代码通常会以更快的速度(可能是由于排序的原因)来进行编码。

用C ++实现的

xor_swap稍微慢一点,因为由于别名,编译器会为每个指令生成一个额外的内存加载/存储-如上文所述,如果我们修改xor_swap为{{1 }}(意味着int * __restrict a, int * __restrict ba从不别名),编译器将生成与bstd_swap相同的代码。

tmp_swap尽管使用了最少的指令,但仍然非常慢(比其他任何选项都慢四倍),只是因为xchg_swap不是如果涉及到内存访问,则可以快速运行。


最终,您可以选择使用一些基于自定义程序集的自定义版本(难以理解和维护)还是仅使用xchg(这是相反的选择),并且还可以从标准库设计人员可以提出,例如在较大的类型上使用矢量化。由于这是一亿多次迭代,因此很明显,这里使用汇编代码的潜在改进非常小-如果完全改进(尚不清楚),则最多可以节省几微秒。 / p>

TL; DR :您不应该这样做,只需使用std::swap


附录:std::swap(a, b)

我认为,此时稍微解释一下内联汇编代码可能是有意义的。 __asm__ volatile(在GNU模式下,__asm__就足够了)引入了一块汇编代码。 asm可以确保编译器不会对其进行优化-否则就喜欢删除该块。

volatile有两种形式。其中之一还处理__asm__ volatile标签;我不会在这里解决。另一种形式最多包含四个参数,并用冒号(goto)分隔:

  • 最简单的形式(:)仅转储汇编代码,但实际上并未与其周围的C ++代码进行交互。特别是,您需要猜测如何将变量分配给寄存器,这并不是很好。
  • 请注意,汇编代码指令用__asm__ volatile ("rdtsc")分隔,因为该汇编代码被逐字传递到GNU汇编程序("\n")。
  • 第二个参数是输出操作数的列表。您可以指定它们具有的“类型”(特别是gas表示“任何寄存器操作数”,而=r表示“任何寄存器操作数,但它也用作输入”)。例如,+r告诉编译器用包含: "+r" (a), "+r" (b)的寄存器替换%0(引用操作数的第一个),并用包含{{1}的寄存器替换a }。
  • 此符号表示您需要用%1替换b(通常在AT&T汇编符号中引用%eax),以逃避百分号。
  • 如果愿意,还可以使用eax切换到Intel的汇编语法。
  • 第三个参数相同,但是只处理输入操作数。
  • 第四个参数告诉编译器哪些寄存器和存储器位置会丢失其值,从而可以优化汇编代码。例如,“ clobbering” %%eax可能会提示编译器插入完整的内存屏障。您可以看到我将用于临时存储的所有寄存器都添加到了此列表中。