首页 > 解决方案 > GCC 似乎更喜欢比较小的直接值。有没有办法避免这种情况?

问题描述

首先是一个微不足道的数学事实:给定整数nm,我们有n < m当且仅当 ,n <= m - 1

GCC 似乎更喜欢绝对值较小的直接值。因此,当m已知且满足其他条件时,编译器会在等效比较表达式中选择一个最小化绝对值的表达式。例如,它n <= 1000n < 1001GCC 9.2 更喜欢这个

bool f(uint32_t n) {
  return n < 1001;
}

进入这个x86汇编代码

f(unsigned int):
  cmpl $1000, %edi
  setbe %al
  ret

这可能有良好的性能原因,但这不是我的问题。我想知道的是:有没有办法强制 GCC 保持原来的比较?更具体地说,我不担心可移植性,因此 GCC 细节(选项、编译指示、属性……)对我来说是可以的。但是,我正在寻找一种constexpr似乎排除 inline 的友好解决方案asm。最后,我的目标是 C++17,它不包括std::is_constant_evaluated. (话虽如此,请随意提供答案,不管我的限制如何,因为它可能对其他人仍然有用。)

你可能会问我为什么要做这样的事情。开始了。据我了解(如果我错了,请纠正我)这种行为可能是x86_64以下示例中的“悲观”:

bool g(uint64_t n) {
  n *= 5000000001;
  return n < 5000000001;
}

由 GCC 6.2 翻译成

g(unsigned long):
  movabsq $5000000001, %rax
  imulq %rax, %rdi
  movabsq $5000000000, %rax
  cmpq %rax, %rdi
  setbe %al
  ret

x86_64中,使用 64 位立即数的计算有一些限制,可能意味着这些值要加载到寄存器中。在上面的例子中,这发生了两次:常量50000000015000000000存储在rax乘法和比较中。如果 GCC 保留 C++ 代码中出现的原始比较(即反对5000000001),则不需要第二个movabs.

这也意味着代码大小的损失,我猜这被认为是一个问题,并且最新版本的 GCC(例如 9.2)产生了这个:

g(unsigned long):
  movabsq $5000000001, %rax
  imulq %rax, %rdi
  subq $1, %rax
  cmpq %rax, %rdi
  setbe %al
  ret

因此,10movabs字节长的指令被 4 字节长的subq指令取代。无论如何,subq也似乎没有必要。

标签: c++assemblygccx86immediate-operand

解决方案


通常,强制 GCC 输出您可能想要的特定指令序列的唯一方法是使用汇编。如果 GCC 没有生成最佳代码,最好的办法是提交错误报告,希望它在未来的版本中得到改进。

对于您的特定情况,至少有一种解决方法可以生成您想要的代码,至少对于当前的编译器而言。虽然它不需要在汇编中编写整个代码序列,但它确实使用内联汇编将常量加载到寄存器中,假设 GCC 更喜欢使用该寄存器而不是生成新的常量值进行比较。

#include <stdint.h>

static uint64_t 
load_const_asm(uint64_t c) {
    if (__builtin_constant_p(c) && (int32_t) c != c) {
           asm("" : "+r" (c));
    }
    return c;
}

bool
g(uint64_t n) {
    uint64_t c = load_const_asm(5000000001);
    n *= c;
    return n < c;
}

-O在 GCC 9.2 上编译时生成以下代码:

_Z1gm:
  movabs rax, 5000000001
  imul rdi, rax
  cmp rdi, rax
  setb al
  ret

这个 asm 语句欺骗编译器不生成常量减一的单独加载,因为编译器不知道 asm 语句的作用,即使它只是一个空字符串。它强制将常量放入寄存器,但编译器不知道在 asm 语句“执行”之后寄存器将包含什么。

与使用带有 CMP 指令的 asm 语句相比,这样做的优势在于它为编译器提供了最大的自由度来优化您的代码。这通常是内联汇编的一大缺点,它很容易最终使您的代码感到悲观。例如,使用内联汇编可以防止编译器在编译时计算 g() 的结果,如果n是一个常量。但是,对于我上面的代码示例,它仍然能够确定如果 g()n为 1,则它必须返回 true。

最后,确保您没有试图过早地优化您的代码。如果它不会对您的应用程序的性能产生任何明显的不同,您不希望像这样混淆您的代码。如果您最终使用了此代码或其他一些 hack,那么正如 Peter Cordes 在评论中所说,您应该清楚地注释您的代码以解释为什么需要 hack,以便在不再需要时将其删除。


推荐阅读