【问题标题】:How to force compiler to generate conditional move by using inline assembly如何使用内联汇编强制编译器生成条件移动
【发布时间】:2020-06-24 14:33:17
【问题描述】:

我花了几个小时试图将以下代码转换为内联汇编 (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"
);

【问题讨论】:

  • 你是如何调用 GCC 的?
  • "寄存器损坏有一些问题,我无法正确处理。" 这不是问题描述。您看到的具体问题是什么?
  • 你试过像low = (middle &lt; key) ? new_low : low;这样的三元运算符吗?您是否尝试过配置文件引导优化,因此 GCC 可以看到该分支实际上不是很可预测? (gcc optimization flag -O3 makes code slower than -O2 表明 if 转换为无分支通常仅在 -O3 处完成,至少在某些情况下)。另外,请记住,无分支会创建数据依赖关系,这对于二分搜索可能会更糟;推测执行有效地为您提供内存并行/预取,而不是序列化加载。
  • 使用使用low 加上size 而不是lowhigh 的二分搜索。这意味着您只需要一个条件(如果针在高半部分,则更新基数),并且可以无条件更新大小(减半)。这通常会导致cmov

标签: gcc x86 inline-assembly conditional-move


【解决方案1】:

您通常不需要内联 asm,但您的问题是 [low] "=&amp;r"(low) 是只写输出!你告诉编译器变量的旧值是不相关的,它是只写的。但cmov 并非如此:它是一个 3 输入指令:src、dst 和 FLAGS。

使用"+&amp;r" 读/写操作数作为低/高(cmov 目标)。原来这基本上是 how to force the use of cmov in gcc and VS 的副本,它显示工作内联 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 &lt; key) ? new_low : low;?您是否尝试过配置文件引导优化,因此 GCC 可以看到该分支实际上不是很可预测? (gcc optimization flag -O3 makes code slower than -O2 表明 if 转换为无分支通常只在 -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 快速搜索宽隐式树以平衡计算与延迟,从而只需几个步骤即可获得成功。它是有序的,因此您可以按顺序遍历它,或者在获得命中后找到上一个或下一个元素。


相关:

【讨论】:

  • 你给我的建议解决了这个问题。谢谢你。正是你所说的发生了:对于小型工作集,如果更快,则带有条件分支的版本,但是一旦工作集不适合缓存,带有分支的版本就会更快。你能指出我更多的资源,为什么会这样?
  • 另外,无论我在做什么,编译器(GCC 4.9 和 GCC 8)都不会生成 cmov。
  • @Bogi: agner.org/optimize 是我理解乱序执行的最佳建议。也许lighterra.com/papers/modernmicroprocessors 了解有关 CPU 工作原理的一些基础知识。具体二分查找见About the branchless binary search
  • 谢谢,彼得!我写了一篇关于分支优化的文章,你指出的是其中的关键发现:johnysswlab.com/…
  • 谢谢,彼得!我写了一篇关于使用分支进行优化的文章,您指出的是其中的关键发现:johnysswlab.com/…
猜你喜欢
  • 2019-05-11
  • 2013-07-23
  • 1970-01-01
  • 2014-05-29
  • 1970-01-01
  • 2023-03-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多