要使用内联汇编,您应该使用__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" 可能会提示编译器插入完整的内存栅栏。你可以看到我将所有用于临时存储的寄存器都添加到了这个列表中。