减法和检测下溢,最有效的方法? (带有 GCC 的 x86/64)
Posted
技术标签:
【中文标题】减法和检测下溢,最有效的方法? (带有 GCC 的 x86/64)【英文标题】:Subtract and detect underflow, most efficient way? (x86/64 with GCC) 【发布时间】:2014-07-25 14:48:56 【问题描述】:我正在使用 GCC 4.8.1 编译 C 代码,我需要检测 x86/64 架构的减法中是否发生下溢。两者都是未签名的。我知道在汇编中很容易,但我想知道我是否可以在 C 代码中完成它并让 GCC 以某种方式对其进行优化,因为我找不到它。这是一个非常常用的功能(或低级,是这个词吗?)所以我需要它高效,但 GCC 似乎太笨了,无法识别这个简单的操作?我尝试了很多方法在 C 中给它提示,但它总是使用两个寄存器,而不仅仅是一个 sub 和一个条件跳转。老实说,看到如此愚蠢的代码写了很多次,我很生气(函数被称为 lot)。
我在 C 中的最佳方法似乎如下:
if((a-=b)+b < b)
// underflow here
基本上就是从a中减去b,如果结果下溢检测到它并做一些条件处理(与a的值无关,比如会带来错误等)。
GCC 似乎太笨了,无法将上述内容简化为子跳转和条件跳转,相信我,我尝试了很多方法在 C 代码中执行此操作,并尝试了很多命令行选项(包括 -O3 和 -Os课程)。 GCC 所做的事情是这样的(英特尔语法汇编):
mov rax, rcx ; 'a' is in rcx
sub rcx, rdx ; 'b' is in rdx
cmp rax, rdx ; useless comparison since sub already sets flags
jc underflow
不用说上面是愚蠢的,当它只需要这样的时候:
sub rcx, rdx
jc underflow
这太烦人了,因为 GCC 确实理解 sub 以这种方式修改标志,因为如果我将其类型转换为“int”,它将生成与上面完全相同的内容,除了它使用带符号跳转的“js”,而不是进位,如果无符号值差异足够高以设置高位,这将不起作用。尽管如此,它表明它知道影响这些标志的子指令。
现在,也许我应该放弃尝试让 GCC 正确优化这一点,并使用我没有问题的内联汇编来做到这一点。不幸的是,这需要“asm goto”,因为我需要有条件的 JUMP,而 asm goto 的输出效率不是很高,因为它是易变的。
我尝试了一些东西,但我不知道它是否“安全”使用。 asm goto 由于某种原因不能有输出。我不想让它将所有寄存器刷新到内存,这会扼杀我这样做的全部意义,这是效率。但是,如果我使用空的 asm 语句,并且在它之前和之后将输出设置为“a”变量,那会起作用吗?它安全吗?这是我的宏:
#define subchk(a,b,g) typeof(a) _a=a; \
asm("":"+rm"(_a)::"cc"); \
asm goto("sub %1,%0;jc %l2"::"r,m,r"(_a),"r,r,m"(b):"cc":g); \
asm("":"+rm"(_a)::"cc");
并像这样使用它:
subchk(a,b,underflow)
// normal code with no underflow
// ...
underflow:
// underflow occured here
它有点难看,但效果很好。在我的测试场景中,它只编译 FINE 而没有易失性开销(将寄存器刷新到内存)而不会产生任何不好的东西,而且它似乎工作正常,但这只是一个有限的测试,我不可能在任何地方测试这个我使用这个函数/macro 正如我所说的,它被使用了很多,所以我想知道是否有人知识渊博,上述构造是否存在不安全的地方?
特别是,如果发生下溢,则不需要 'a' 的值,因此考虑到这一点,我的内联 asm 宏是否会发生任何副作用或不安全的事情?如果不是,我会毫无问题地使用它,直到他们优化编译器,以便我猜到之后可以将其替换回来。
请不要将此变成关于过早优化或其他问题的辩论,请继续关注这个问题,我完全了解这一点,所以谢谢。
【问题讨论】:
我认为问题在于你假设编译器总是优化到做某事的“最佳”方式,缺陷是你的假设不是编译器也不是优化器。 gcc,是开源的...如果你不喜欢它,改变它...if ((r = x - y) > x)
模式更好。实际上,这是下面的答案之一。
既然都没有签名,那(a < b)
有什么问题?
这是因为我想先从 a 中减去 b,如果我这样做 (a
【参考方案1】:
我可能错过了一些明显的东西,但为什么这不是很好?
extern void underflow(void) __attribute__((noreturn));
unsigned foo(unsigned a, unsigned b)
unsigned r = a - b;
if (r > a)
underflow();
return r;
我已经检查过了,gcc 将其优化为您想要的:
foo:
movl %edi, %eax
subl %esi, %eax
jb .L6
rep
ret
.L6:
pushq %rax
call underflow
当然你可以随心所欲地处理下溢,我刚刚这样做是为了保持 asm 简单。
【讨论】:
非常感谢,看来 GCC 很聪明,可以只优化“(a-b) > a”的这种特殊情况,但不是我尝试使用 a _a; ) 然后我只使用 if(subchk(A, B)) 下溢; 。我使用宏是因为我希望它与类型无关。出于某种原因,GCC 有时在我的测试中现在使用“mov rsi,local_var”,然后使用“sub rsi,B”(我想要的子),并将值放回本地堆栈“mov local_var,rsi”,然后执行跳。不过,现在这肯定是另一回事了。【参考方案2】:下面的汇编代码怎么样(可以包装成GCC格式):
sub rcx, rdx ; assuming operands are in rcx, rdx
setc al ; capture carry bit int AL (see Intel "setxx" instructions)
; return AL as boolean to compiler
然后调用/内联汇编代码,并在生成的布尔值上进行分支。
【讨论】:
您的解决方案很好,我也想到了,唯一的问题是,以这种方式使用内联 asm 会使编译器无法优化此类代码(因为它不知道),在这种特殊情况下,下溢检测是通过特殊的代码/错误处理来完成的,这不能仅仅通过修改返回值来完成——除非,我检查返回值,然后根据它跳转,但在那种情况下它比我试图避免使用 GCC 的“sub/cmp”组合。不过还是谢谢。【参考方案3】:您是否测试过这是否实际上更快?现代 x86 微架构使用 microcode,将单个汇编指令转换为更简单的微操作序列。他们中的一些人还进行微操作融合,其中将一系列组装指令转换为单个微操作。特别是,像test %reg, %reg; jcc target
这样的序列被融合了,可能是因为全局处理器标志是性能的祸根。
如果 cmp %reg, %reg; jcc target
是 mOp 融合的,gcc 可能会使用它来获得更快的代码。根据我的经验,gcc非常擅长调度和类似的低级优化。
【讨论】:
我认为它更快,因为 sub 就像 cmp 一样,只是它保留了结果,而且我认为 GCC 不知道这里隐藏的东西,因为如果我进行签名检查(即将它们类型转换为签名,然后检查是否小于 0),它只会做一个 sub,然后是一个“js”来跳转(符号位 = 负),如果 cmp/sub 组合更快,它肯定会这样做。感谢您对微操作的见解,我不知道,我做了一个简短的研究,现在看起来很有趣。以上是关于减法和检测下溢,最有效的方法? (带有 GCC 的 x86/64)的主要内容,如果未能解决你的问题,请参考以下文章