小编典典

为什么 GCC 会为几乎相同的 C 代码生成如此完全不同的程序集?

all

在编写优化ftol函数时,我发现GCC 4.6.1. 让我先向您展示代码(为清楚起见,我标记了差异):

快速截断,C:

int fast_trunc_one(int i) {
    int mantissa, exponent, sign, r;

    mantissa = (i & 0x07fffff) | 0x800000;
    exponent = 150 - ((i >> 23) & 0xff);
    sign = i & 0x80000000;

    if (exponent < 0) {
        r = mantissa << -exponent;                       /* diff */
    } else {
        r = mantissa >> exponent;                        /* diff */
    }

    return (r ^ -sign) + sign;                           /* diff */
}

fast_trunc_two,C:

int fast_trunc_two(int i) {
    int mantissa, exponent, sign, r;

    mantissa = (i & 0x07fffff) | 0x800000;
    exponent = 150 - ((i >> 23) & 0xff);
    sign = i & 0x80000000;

    if (exponent < 0) {
        r = (mantissa << -exponent) ^ -sign;             /* diff */
    } else {
        r = (mantissa >> exponent) ^ -sign;              /* diff */
    }

    return r + sign;                                     /* diff */
}

好像一样吧?那么GCC不同意。编译后gcc -O3 -S -Wall -o test.s test.c是汇编输出:

fast_trunc_one,生成:

_fast_trunc_one:
LFB0:
    .cfi_startproc
    movl    4(%esp), %eax
    movl    $150, %ecx
    movl    %eax, %edx
    andl    $8388607, %edx
    sarl    $23, %eax
    orl $8388608, %edx
    andl    $255, %eax
    subl    %eax, %ecx
    movl    %edx, %eax
    sarl    %cl, %eax
    testl   %ecx, %ecx
    js  L5
    rep
    ret
    .p2align 4,,7
L5:
    negl    %ecx
    movl    %edx, %eax
    sall    %cl, %eax
    ret
    .cfi_endproc

fast_trunc_two,生成:

_fast_trunc_two:
LFB1:
    .cfi_startproc
    pushl   %ebx
    .cfi_def_cfa_offset 8
    .cfi_offset 3, -8
    movl    8(%esp), %eax
    movl    $150, %ecx
    movl    %eax, %ebx
    movl    %eax, %edx
    sarl    $23, %ebx
    andl    $8388607, %edx
    andl    $255, %ebx
    orl $8388608, %edx
    andl    $-2147483648, %eax
    subl    %ebx, %ecx
    js  L9
    sarl    %cl, %edx
    movl    %eax, %ecx
    negl    %ecx
    xorl    %ecx, %edx
    addl    %edx, %eax
    popl    %ebx
    .cfi_remember_state
    .cfi_def_cfa_offset 4
    .cfi_restore 3
    ret
    .p2align 4,,7
L9:
    .cfi_restore_state
    negl    %ecx
    sall    %cl, %edx
    movl    %eax, %ecx
    negl    %ecx
    xorl    %ecx, %edx
    addl    %edx, %eax
    popl    %ebx
    .cfi_restore 3
    .cfi_def_cfa_offset 4
    ret
    .cfi_endproc

这是一个 极端 的区别。这实际上也显示在配置文件上,fast_trunc_onefast_trunc_two.
现在我的问题是:是什么原因造成的?


阅读 134

收藏
2022-07-28

共1个答案

小编典典

更新以与 OP 的编辑同步

通过修改代码,我设法了解 GCC 如何优化第一种情况。

在我们了解它们为什么如此不同之前,首先我们必须了解 GCC 是如何优化fast_trunc_one().

信不信由你,fast_trunc_one()正在对此进行优化:

int fast_trunc_one(int i) {
    int mantissa, exponent;

    mantissa = (i & 0x07fffff) | 0x800000;
    exponent = 150 - ((i >> 23) & 0xff);

    if (exponent < 0) {
        return (mantissa << -exponent);             /* diff */
    } else {
        return (mantissa >> exponent);              /* diff */
    }
}

这会产生与原始程序完全相同的程序集fast_trunc_one()- 注册名称和所有内容。

请注意,在程序集中没有xors for fast_trunc_one()。这就是给我的。


怎么会这样?


步骤1: sign = -sign

首先,让我们看一下sign变量。由于sign = i & 0x80000000;,只有两个可能的值sign可以取:

  • sign = 0
  • sign = 0x80000000

现在认识到,在这两种情况下,sign == -sign. 因此,当我将原始代码更改为:

int fast_trunc_one(int i) {
    int mantissa, exponent, sign, r;

    mantissa = (i & 0x07fffff) | 0x800000;
    exponent = 150 - ((i >> 23) & 0xff);
    sign = i & 0x80000000;

    if (exponent < 0) {
        r = mantissa << -exponent;
    } else {
        r = mantissa >> exponent;
    }

    return (r ^ sign) + sign;
}

它产生与原始的完全相同的程序集fast_trunc_one()。我会省去你的程序集,但它是相同的 - 注册名称和所有。


第 2 步: 数学简化:x + (y ^ x) = y

sign只能取两个值之一,00x80000000

  • x = 0x + (y ^ x) = y则微不足道成立。
  • 添加和异或是0x80000000相同的。它翻转符号位。因此x + (y ^ x) = y也成立时x = 0x80000000

因此,x + (y ^ x)减少到y。代码简化为:

int fast_trunc_one(int i) {
    int mantissa, exponent, sign, r;

    mantissa = (i & 0x07fffff) | 0x800000;
    exponent = 150 - ((i >> 23) & 0xff);
    sign = i & 0x80000000;

    if (exponent < 0) {
        r = (mantissa << -exponent);
    } else {
        r = (mantissa >> exponent);
    }

    return r;
}

同样,这将编译为完全相同的程序集 - 注册名称和所有。


上述版本最终简化为:

int fast_trunc_one(int i) {
    int mantissa, exponent;

    mantissa = (i & 0x07fffff) | 0x800000;
    exponent = 150 - ((i >> 23) & 0xff);

    if (exponent < 0) {
        return (mantissa << -exponent);             /* diff */
    } else {
        return (mantissa >> exponent);              /* diff */
    }
}

这几乎正​​是 GCC 在程序集中生成的。


那么为什么编译器不优化fast_trunc_two()相同的东西呢?

其中的关键部分fast_trunc_one()x + (y ^ x) = y优化。在表达式fast_trunc_two()x + (y ^ x)被拆分跨分支。

我怀疑这可能足以让 GCC 不进行这种优化。(它需要将^ -sign分支吊出并将其合并到r + sign最后。)

例如,这会产生与以下相同的程序集fast_trunc_one()

int fast_trunc_two(int i) {
    int mantissa, exponent, sign, r;

    mantissa = (i & 0x07fffff) | 0x800000;
    exponent = 150 - ((i >> 23) & 0xff);
    sign = i & 0x80000000;

    if (exponent < 0) {
        r = ((mantissa << -exponent) ^ -sign) + sign;             /* diff */
    } else {
        r = ((mantissa >> exponent) ^ -sign) + sign;              /* diff */
    }

    return r;                                     /* diff */
}
2022-07-28