5

首先是一个微不足道的数学事实:给定整数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也似乎没有必要。

4

3 回答 3

2

通常,强制 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,以便在不再需要时将其删除。

于 2019-12-01T06:34:50.633 回答
2

这是一个 C++20 解决方案,遗憾的是,我无法使用

#include <cstdint>
#include <type_traits>

template <class U>
bool less_asm(U n, U m) noexcept {
  bool r;
  asm("cmp%z[m]\t{%[m], %[n]|%[n], %[m]}"
    : "=@ccb"(r) : [n]"r"(n), [m]"re"(m) : "cc");
  return r;
}

template <class U>
constexpr bool less(U n, U m) noexcept {
  if (std::is_constant_evaluated())
    return n < m;
  return less_asm(n, m);
}

static_assert(less(uint64_t(0), uint64_t(1)));

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

GCC 9.2(使用 -O2 -std=c++2a)生成:

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

更新:下面的代码片段显示了两个改进:

  1. 它适用于 C++17,但需要 GCC-9.1 或更高版本。(感谢Peter Cordes建议使用__builtin_constant_p()而不是.GCC 的早期std::is_constant_evaluated()版本抱怨asm出现在constexpr函数中。)
  2. 它在范围内引入了更少的名称。(通过使用 lambda 而不是less_asm。)


template <class U>
constexpr bool less(U n, U m) noexcept {
  if (__builtin_constant_p(n < m))
    return n < m;
  return [&]{
    bool r;
    asm("cmp\t{%[m], %[n]|%[n], %[m]}"
      : "=@ccb"(r) : [n]"r"(n), [m]"re"(m) : "cc");
    return r;
  }();
}
于 2019-11-30T15:30:40.253 回答
-3

带有 -O2 的 Linux 上的 gcc 9.2.1 给出:

movabsq $5000000001, %rax
imulq   %rax, %rdi
subq    $1, %rax
cmpq    %rax, %rdi
setbe   %al
ret

因此,除非您真的担心减法,否则您似乎不需要做任何特别的事情。

于 2019-11-30T15:41:40.927 回答