4

我正在使用 GCC 4.8.1 编译 C 代码,我需要检测 x86/64 架构的减法中是否发生下溢。两者都是未签名的。我知道在汇编中很容易,但我想知道我是否可以在 C 代码中完成它并让 GCC 以某种方式对其进行优化,因为我找不到它。这是一个非常常用的功能(或低级,是这个词吗?)所以我需要它高效,但 GCC 似乎太笨了,无法识别这个简单的操作?我尝试了很多方法在 C 中给它提示,但它总是使用两个寄存器,而不仅仅是一个 sub 和一个条件跳转。老实说,看到如此愚蠢的代码编写了很多次(函数被调用了很多次),我感到很恼火。

我在 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 宏是否会发生任何副作用或不安全的事情?如果不是,我会毫无问题地使用它,直到他们优化编译器,所以我猜后可以将其替换回来。

请不要将其变成关于过早优化或其他问题的辩论,请继续关注问题的主题,我完全了解这一点,所以谢谢。

4

3 回答 3

4

我可能错过了一些明显的东西,但为什么这不好?

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 简单。

于 2014-07-25T15:34:56.543 回答
0

下面的汇编代码怎么样(你可以把它包装成 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  

然后调用/内联汇编代码,并在生成的布尔值上进行分支。

于 2014-07-25T15:02:26.800 回答
0

您是否测试过这实际上是否更快?现代 x86 微架构使用微码,将单个汇编指令转换为更简单的微操作序列。他们中的一些人还进行微操作融合,其中将一系列组装指令转换为单个微操作。特别是,像这样的序列test %reg, %reg; jcc target被融合了,可能是因为全局处理器标志是性能的祸根。
如果cmp %reg, %reg; jcc target是 mOp-fused,gcc 可能会使用它来获得更快的代码。根据我的经验,gcc非常擅长调度和类似的低级优化。

于 2014-07-25T16:13:03.943 回答