c - 为什么 GCC 会为几乎相同的 C 代码生成如此截然不同的程序集?

标签 c gcc assembly x86 compiler-optimization

在编写优化的 ftol 函数时,我在 GCC 4.6.1 中发现了一些非常奇怪的行为。让我先向您展示代码(为了清楚起见,我标记了不同之处):

fast_trunc_one, 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 -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 快大约 30%。现在我的问题是:造成这种情况的原因是什么?

最佳答案

已更新以与 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() 完全相同的程序集 - 注册名称和所有内容。

请注意,fast_trunc_one() 的程序集中没有xor。这就是它送给我的原因。


怎么会这样?


第 1 步: sign = -sign

首先,让我们看一下sign 变量。由于 sign = i & 0x80000000;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 = 0 时,则 x + (y ^ x) = y 那么平凡成立。
  • 0x80000000 的加法和异或操作是一样的。它翻转符号位。因此 x + (y ^ x) = yx = 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 */
}

关于c - 为什么 GCC 会为几乎相同的 C 代码生成如此截然不同的程序集?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/10250419/

相关文章:

macos - Cython:prange 正在重复而不是并行化

assembly - 实模式中断处理例程未按预期工作

c - 为什么编译器要向已经 4 字节对齐的结构添加填充?

c++ - 如何安排这个多线程应用程序?

gcc - 如何在 Ubuntu 上使用 DSFML2 和 D2 解决链接器错误?

c++ - 有没有比添加 0.5f 和截断转换更直接的方法来将 float 转换为 int 并进行舍入?

debugging - ollydbg 操作码跟踪

C http 服务器在不可预测的时间后停止

c - 为什么在使用共享内存的生产者-消费者范式中缓冲区中只允许 BUFFER_SIZE-1 项?

c - mingw 获取光标位置(控制台)