c++ - GCC 似乎更喜欢比较小的直接值。有没有办法避免这种情况?
问题描述
首先是一个微不足道的数学事实:给定整数n
和m
,我们有n < m
当且仅当 ,n <= m - 1
。
GCC 似乎更喜欢绝对值较小的直接值。因此,当m
已知且满足其他条件时,编译器会在等效比较表达式中选择一个最小化绝对值的表达式。例如,它n <= 1000
比n < 1001
GCC 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 位立即数的计算有一些限制,可能意味着这些值要加载到寄存器中。在上面的例子中,这发生了两次:常量5000000001
和5000000000
存储在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
也似乎没有必要。
解决方案
通常,强制 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,以便在不再需要时将其删除。
推荐阅读
- awk - 使用 awk 插入新数据并添加多远
- apache-spark - 我可以用 shc 显示表模式吗(Spark Hbase 连接器)
- linux - 在 cgroup 中找不到“cpu.cfs_period_us”和“cpu.cfs_quota_us”
- r - 将“04.05.2003 23:00:00.000 GMT+0200”转换为 R 中的日期变量
- python - 如何将参数传递给 FastAPI 中间件并影响其处理逻辑?
- authentication - 事后证明下载文件真实性的方法
- django - 如何在使用 django 多租户时在 setting.py 文件中动态设置不同的参数?
- python - 当尝试使用网格搜索交叉验证优化我的分类器模型时,我的 f1 分数降至 0
- java - 如何使用 kotlin 以单一方法获取 android 屏幕上按钮的所有事件?
- ios - Xcode PhaseScriptExecution 失败,退出代码非零