【问题标题】:Correct way to implement inline assembler in c++ for xor operations on variables在c ++中实现内联汇编程序以对变量进行异或操作的正确方法
【发布时间】:2019-02-17 12:59:23
【问题描述】:

我最近看到一篇关于如何使用异或而不是使用临时变量来执行交换操作的文章。当我使用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 操作,因为它们比仅仅执行三个需要三个周期的 xor 操作需要更多的 cpu 周期。我怎么能做到这一点?

【问题讨论】:

  • 为什么要在汇编中这样做? a ^ b; b ^ a; a ^ b; 也会这样做。
  • 说真的,只需使用std::swap(a,b);,不要试图智取编译器。许多架构都有专用的exchange instruction,否则复制到临时寄存器通常会快得多。
  • 在您的代码中,volatile 强制将每个操作的操作数和结果从每行的内存读取/写入内存。为什么a 和b volatile 在你的例子中?
  • godbolt.org/z/Oxz-0V ... 问题是,gcc 确实认识到它是“交换”操作,并使用 mov 指令和临时寄存器以更快的变体将其重写为机器代码,它将拒绝发出不太理想的xor 变体。所以这只是演示如何让 -O3 实际上做一些明智的事情,而不是删除所有代码。如果你写错了 C++,那么“通过内联汇编优化它”就没有意义了,那是最后一步,而不是第一步。如果你想玩组装,就像你实际上想要慢速 xor 交换,那么使用相当独立的组装。
  • @TheSupremeGod 1 asm 指令不与 1 个 CPU 周期相同。有些指令需要数十、数百(或更多)周期才能完成。有些只需要一些。您无法仅通过计算指令数来衡量效率。

标签: c++ assembly inline xor


【解决方案1】:

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

所以,我实现了一堆“交换”函数,一些在 C++ 中,一些在汇编中,并进行了一些测量,每个函数连续运行 1 亿次。


测试用例

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)

异或交换

这就是你在 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 指出 cmets 中的这个缺陷)。

与临时变量交换

这是带有临时变量的“标准”交换(编译器会立即优化为与 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。

汇编中的异或交换

我在 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_swap、tmp_swap、xor_asm_swap 和xor_asm_register_swap 在速度上通常非常相似——事实上,如果我将xor_asm_register_swap 移到前面,结果会比@ 稍慢987654352@。另请注意,tmp_swap 与 std_swap 的汇编代码完全相同(尽管它通常会更快一些,可能是因为排序的原因)。

在 C++ 中实现的xor_swap 稍微慢一些,因为编译器会因为别名而为每条指令生成额外的内存加载/存储 - 如上所述,如果我们修改 xor_swap 以改为采用 int * __restrict a, int * __restrict b(这意味着a 和 b 从不别名),编译器生成与 std_swap 和 tmp_swap 相同的代码。

xchg_swap,尽管使用最少的指令,非常慢(比任何其他选项慢四倍以上),因为xchg 不是一个快速操作,如果它涉及内存访问。


最终,您可以选择使用一些自定义的基于程序集的版本(难以理解和维护)或仅使用 std::swap(几乎相反,并且还受益于标准库的任何优化)设计师可以想出,例如在更大的类型上使用矢量化)。由于这是超过一亿次迭代,应该清楚的是,在这里使用汇编代码的潜在改进非常小 - 如果你完全改进(不清楚)你最多会减少几微秒。

TL;DR:你不应该这样做,只需使用std::swap(a, b)


附录:__asm__ volatile

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

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

  • 最简单的形式 (__asm__ volatile ("rdtsc")) 只是转储汇编代码,但并不真正与周围的 C++ 代码交互。特别是,您需要猜测变量是如何分配给寄存器的,这并不完全好。
  • 请注意,汇编代码指令用"\n" 分隔,因为此汇编代码逐字传递给 GNU 汇编器 (gas)。
  • 第二个参数是输出操作数的列表。您可以指定它们的“类型”(特别是,=r 表示“任何寄存器操作数”,+r 表示“任何寄存器操作数,但它也用作输入”)。例如,: "+r" (a), "+r" (b) 告诉编译器将%0(引用第一个操作数)替换为包含a 的寄存器,并将%1 替换为包含b 的寄存器。
  • 此表示法意味着您需要将 %eax(通常在 AT&T 汇编表示法中引用 eax)替换为 %%eax 以转义百分号。
  • 如果您愿意,也可以使用".intel_syntax\n" 切换到英特尔的汇编语法。
  • 第三个参数相同,但只处理输入操作数。
  • 第四个参数告诉编译器哪些寄存器和内存位置会丢失它们的值以启用围绕汇编代码的优化。例如,“破坏”"memory" 可能会提示编译器插入完整的内存栅栏。你可以看到我将所有用于临时存储的寄存器都添加到了这个列表中。

【讨论】:

  • 请注意,如果您使用xchg 来交换两个寄存器中的值,它实际上非常快。
  • 请注意,您的 C++ xor_swap 源代码不会向编译器发出两个指针永远不会别名的信号,这将允许编译器完全避免 xor 指令并使用仅 mov 的交换代码变体。 .. 试试void xor_swap(int * __restrict a, int *b),看看它是如何像代码一样折叠回std::swap(给程序员施加压力,永远不要用别名相同的内存调用它......实际上在swap的情况下,甚至用别名内存调用它确实会产生“正确”的结果,因为相同的值与相同的值交换,但通常)OP 假设两个不同的变量。
  • 您的答案在显示xor 变体方面实际上很棒。但是如果一个人试图“公平”,并使用完整的 C++ 专业知识,那么 OP 会假设两个不同的内存变量,所以让编译器知道是公平的,然后编译器会更好地理解机器,并避免更慢xor完全变种。编写正确的 C++ 非常棘手(为编译器提供相同的假设,编码器在编写汇编变体时使用这些假设)。不幸的是,编写一些(非最佳)C++ 并显示更好的汇编和责备编译器很容易,我也做过很多次。 :)
  • @TheSupremeGod: asm volatile 在这里毫无意义。它是输入的纯函数,如果输出操作数未使用,您希望编译器对其进行优化。或者 CSE 多次交换相同的输入。当然,这就是为什么你一开始就不想使用内联汇编的原因;使用std::swap,编译器已经做得更好了。
  • (实际上,此版本中的 volatile 可能是解决其中错误的部分但不足的解决方法:将寄存器中的指针传递给内联 asm 并 not 暗示内联 asm 读取指向的内存。没有"memory" clobber,gcc/clang 将优化掉明显死的存储到指向的内存中,或者至少用 asm 重新排序它们。这就是为什么你应该使用@987654400 @读/写内存操作数。)见Informing clang that inline asm reads a region of mem和Looping over arrays with inline asm
猜你喜欢
  • 2013-01-15
  • 1970-01-01
  • 1970-01-01
  • 2012-11-10
  • 2013-04-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多