如何使用内联汇编强制编译器生成条件移动
How to force compiler to generate conditional move by using inline assembly
我花了几个小时尝试将以下代码转换为内联汇编 (GCC),但没有成功:
int new_low = mid + 1;
int new_high = mid - 1;
if (middle < key) {
low = new_low;
}
if (!(middle < key)) {
high = new_high;
}
我希望编译器优化掉那些 if,而是使用条件移动。不幸的是,编译器一直在生成跳转。我尝试编写内联汇编,但我并不擅长,而且寄存器破坏存在一些问题,我无法正确处理。我做错了什么?
__asm__ (
"cmp %[array_middle], %[key];"
"cmovb %[new_low], %[low];"
"cmovae %[new_high], %[high];"
: [low] "=&r"(low), [high] "=&r"(high)
: [new_low] "r"(new_low), [new_high] "r"(new_high), [array_middle] "r"(middle), [key] "r"(key)
: "cc"
);
你通常不需要内联汇编,但你的问题是 [low] "=&r"(low)
是只写输出!!您是在告诉编译器变量的旧值无关紧要,它是只写的。但对于 cmov
而言并非如此:它是一个 3 输入指令:src、dst 和 FLAGS。
对low/high(cmov 目标)使用"+&r"
read/write 操作数。事实证明,这基本上是 的副本,它显示工作的内联 asm 几乎与您的相同(使用 FP 比较而不是整数,但使用相同的 cmov 指令。)
这应该可行,但强制编译器使用比原本需要更多的寄存器。 (例如,它可以使用 LEA after CMP 来执行 mid+1 和 mid-1,覆盖 mid
。或者甚至使用 LEA + INC 因为 B 和 AE 条件只读CF 和足够新的 CPU 根本没有部分标志停顿,它们只是在必要时让 cmov 分别读取 FLAGS 的两个部分。这就是为什么 cmovbe
在 Skylake 上是一条 2-uop 指令,而对于 1大多数其他条件。)
让编译器无分支:
您是否尝试过三元运算符,例如 low = (middle < key) ? new_low : low;
?您是否尝试过配置文件引导优化,以便 GCC 可以看到分支实际上不是很可预测? ( 表明 if-conversion 到无分支通常只在 -O3
完成,至少在某些情况下是这样。
此外,请记住,无分支会产生数据依赖性,这对于二分查找来说可能更糟;推测执行有效地为您提供内存并行/预取,而不是序列化加载。 (https://agner.org/optimize/)
对于在 L1d 缓存中命中的小型二进制搜索,分支未命中的成本高于加载使用延迟。但是一旦您预计会有一些 L2 缓存未命中,分支恢复比 L3 的加载使用延迟更便宜。 About the branchless binary search.
1/4 和 3/4 元素的软件预取可以帮助缓解这种情况,隐藏加载使用延迟。这利用了内存级并行性(同时有多个负载在运行),即使对于通过需求负载链存在数据依赖性的无分支形式也是如此。
如果您可以选择不同的数据结构,What is the most efficient way to implement a BST in such a way the find(value) function is optimized for random values in the tree on x86? 表明使用 SIMD 可以非常快速地搜索宽隐式树,以平衡计算与延迟,从而只需很少的步骤即可获得成功。它是有序的,所以你可以按顺序遍历它,或者在命中后找到上一个或下一个元素。
相关:
- Conditional move (cmov) in GCC compiler
- Make gcc use conditional moves
我花了几个小时尝试将以下代码转换为内联汇编 (GCC),但没有成功:
int new_low = mid + 1;
int new_high = mid - 1;
if (middle < key) {
low = new_low;
}
if (!(middle < key)) {
high = new_high;
}
我希望编译器优化掉那些 if,而是使用条件移动。不幸的是,编译器一直在生成跳转。我尝试编写内联汇编,但我并不擅长,而且寄存器破坏存在一些问题,我无法正确处理。我做错了什么?
__asm__ (
"cmp %[array_middle], %[key];"
"cmovb %[new_low], %[low];"
"cmovae %[new_high], %[high];"
: [low] "=&r"(low), [high] "=&r"(high)
: [new_low] "r"(new_low), [new_high] "r"(new_high), [array_middle] "r"(middle), [key] "r"(key)
: "cc"
);
你通常不需要内联汇编,但你的问题是 [low] "=&r"(low)
是只写输出!!您是在告诉编译器变量的旧值无关紧要,它是只写的。但对于 cmov
而言并非如此:它是一个 3 输入指令:src、dst 和 FLAGS。
对low/high(cmov 目标)使用"+&r"
read/write 操作数。事实证明,这基本上是
这应该可行,但强制编译器使用比原本需要更多的寄存器。 (例如,它可以使用 LEA after CMP 来执行 mid+1 和 mid-1,覆盖 mid
。或者甚至使用 LEA + INC 因为 B 和 AE 条件只读CF 和足够新的 CPU 根本没有部分标志停顿,它们只是在必要时让 cmov 分别读取 FLAGS 的两个部分。这就是为什么 cmovbe
在 Skylake 上是一条 2-uop 指令,而对于 1大多数其他条件。)
让编译器无分支:
您是否尝试过三元运算符,例如 low = (middle < key) ? new_low : low;
?您是否尝试过配置文件引导优化,以便 GCC 可以看到分支实际上不是很可预测? (-O3
完成,至少在某些情况下是这样。
此外,请记住,无分支会产生数据依赖性,这对于二分查找来说可能更糟;推测执行有效地为您提供内存并行/预取,而不是序列化加载。 (https://agner.org/optimize/)
对于在 L1d 缓存中命中的小型二进制搜索,分支未命中的成本高于加载使用延迟。但是一旦您预计会有一些 L2 缓存未命中,分支恢复比 L3 的加载使用延迟更便宜。 About the branchless binary search.
1/4 和 3/4 元素的软件预取可以帮助缓解这种情况,隐藏加载使用延迟。这利用了内存级并行性(同时有多个负载在运行),即使对于通过需求负载链存在数据依赖性的无分支形式也是如此。
如果您可以选择不同的数据结构,What is the most efficient way to implement a BST in such a way the find(value) function is optimized for random values in the tree on x86? 表明使用 SIMD 可以非常快速地搜索宽隐式树,以平衡计算与延迟,从而只需很少的步骤即可获得成功。它是有序的,所以你可以按顺序遍历它,或者在命中后找到上一个或下一个元素。
相关:
- Conditional move (cmov) in GCC compiler
- Make gcc use conditional moves