c - GCC x86-64 次优汇编输出,为什么?

标签 c gcc assembly x86-64

查看以下代码的汇编输出时(没有优化,-O2 和 -O3 产生非常相似的结果):

int main(int argc, char **argv)
{
    volatile float f1 = 1.0f;
    volatile float f2 = 2.0f;

    if(f1 > f2)
    {
        puts("+");
    }
    else if(f1 < f2)
    {
        puts("-");
    }

    return 0;
}

GCC 做了一些我很难理解的事情:

.LC2:
    .string "+"
.LC3:
    .string "-"
    .text
.globl main
    .type   main, @function
main:
.LFB2:
    pushq   %rbp
.LCFI0:
    movq    %rsp, %rbp
.LCFI1:
    subq    $32, %rsp
.LCFI2:
    movl    %edi, -20(%rbp)
    movq    %rsi, -32(%rbp)
    movl    $0x3f800000, %eax
    movl    %eax, -4(%rbp)
    movl    $0x40000000, %eax
    movl    %eax, -8(%rbp)
    movss   -4(%rbp), %xmm1
    movss   -8(%rbp), %xmm0
    ucomiss %xmm0, %xmm1
    jbe .L9
.L7:
    movl    $.LC2, %edi
    call    puts
    jmp .L4
.L9:
    movss   -4(%rbp), %xmm1
    movss   -8(%rbp), %xmm0
    ucomiss %xmm1, %xmm0
    jbe .L4
.L8:
    movl    $.LC3, %edi
    call    puts
.L4:
    movl    $0, %eax
    leave
    ret

为什么 GCC 将浮点值移动到 xmm0 和 xmm1 两次,并且还运行 ucomiss 两次?

执行以下操作不是更快吗?

.LC2:
    .string "+"
.LC3:
    .string "-"
    .text
.globl main
    .type   main, @function
main:
.LFB2:
    pushq   %rbp
.LCFI0:
    movq    %rsp, %rbp
.LCFI1:
    subq    $32, %rsp
.LCFI2:
    movl    %edi, -20(%rbp)
    movq    %rsi, -32(%rbp)
    movl    $0x3f800000, %eax
    movl    %eax, -4(%rbp)
    movl    $0x40000000, %eax
    movl    %eax, -8(%rbp)
    movss   -4(%rbp), %xmm1
    movss   -8(%rbp), %xmm0
    ucomiss %xmm0, %xmm1
    jb  .L8 # jump if less than
    je  .L4 # jump if equal
.L7:
    movl    $.LC2, %edi
    call    puts
    jmp .L4
.L8:
    movl    $.LC3, %edi
    call    puts
.L4:
    movl    $0, %eax
    leave
    ret

我根本不是真正的汇编程序员,但运行重复的指令对我来说似乎很奇怪。我的代码版本有问题吗?


更新

如果你删除我原来的 volatile 并用 scanf() 替换它,你会得到相同的结果:

int main(int argc, char **argv)
{
    float f1;
    float f2;

    scanf("%f", &f1);
    scanf("%f", &f2);

    if(f1 > f2)
    {
        puts("+");
    }
    else if(f1 < f2)
    {
        puts("-");
    }

    return 0;
}

以及对应的汇编器:

.LCFI2:
    movl    %edi, -20(%rbp)
    movq    %rsi, -32(%rbp)
    leaq    -4(%rbp), %rsi
    movl    $.LC0, %edi
    movl    $0, %eax
    call    scanf
    leaq    -8(%rbp), %rsi
    movl    $.LC0, %edi
    movl    $0, %eax
    call    scanf
    movss   -4(%rbp), %xmm1
    movss   -8(%rbp), %xmm0
    ucomiss %xmm0, %xmm1
    jbe .L9
.L7:
    movl    $.LC1, %edi
    call    puts
    jmp .L4
.L9:
    movss   -4(%rbp), %xmm1
    movss   -8(%rbp), %xmm0
    ucomiss %xmm1, %xmm0
    jbe .L4
.L8:
    movl    $.LC2, %edi
    call    puts
.L4:
    movl    $0, %eax
    leave
    ret

最终更新

在查看了一些后续评论后,han(在 Jonathan Leffler 的帖子下发表了评论)似乎解决了这个问题。 GCC 不进行优化不是因为它不能,而是因为我没有告诉它。似乎这一切都归结为 IEEE 浮点规则和处理严格的条件,GCC 不能简单地在第一个 UCOMISS 之后执行 if above 跳转或 if below 跳转,因为它需要处理 float 的所有特殊条件。当使用 han 推荐的 -ffast-math 优化器时(没有 -Ox 标志启用 -ffast-math 因为它会破坏某些程序)GCC 完全符合我的要求:

以下程序集是使用 GCC 4.3.2“gcc -S -O3 -ffast-math test.c”生成的

.LC0:
    .string "%f"
.LC1:
    .string "+"
.LC2:
    .string "-"
    .text
    .p2align 4,,15
.globl main
    .type   main, @function
main:
.LFB25:
    subq    $24, %rsp
.LCFI0:
    movl    $.LC0, %edi
    xorl    %eax, %eax
    leaq    20(%rsp), %rsi
    call    scanf
    leaq    16(%rsp), %rsi
    xorl    %eax, %eax
    movl    $.LC0, %edi
    call    scanf
    movss   20(%rsp), %xmm0
    comiss  16(%rsp), %xmm0
    ja  .L11
    jb  .L12
    xorl    %eax, %eax
    addq    $24, %rsp
    .p2align 4,,1
    .p2align 3
    ret
    .p2align 4,,10
    .p2align 3
.L12:
    movl    $.LC2, %edi
    call    puts
    xorl    %eax, %eax
    addq    $24, %rsp
    ret
    .p2align 4,,10
    .p2align 3
.L11:
    movl    $.LC1, %edi
    call    puts
    xorl    %eax, %eax
    addq    $24, %rsp
    ret

请注意,两条 UCOMISS 指令现在已替换为一条 COMISS,紧接着是 JA(如果在上方则跳转)和 JB(如果在下方则跳转)。如果你让它使用 -ffast-math,GCC 能够确定这个优化!

UCOMISS 与 COMISS(http://www.softeng.rl.ac.uk/st/archive/SoftEng/SESP/html/SoftwareTools/vtune/users_guide/mergedProjects/analyzer_ec/mergedProjects/reference_olh/mergedProjects/instructions/instruct32_hh/vc315.htm):“UCOMISS 指令与 COMISS 指令的不同之处在于它仅在源操作数为 SNaN 时才发出无效 SIMD 浮点异常信号。如果源操作数是 QNaN 或 COMISS 指令则发出无效信号SNaN。”

再次感谢大家的有益讨论。

最佳答案

还有一个原因: 如果你仔细看一下,它不是同一个表达式。

它们不是彼此的补充。因此,无论如何您都必须进行两次比较。 volatile 将强制重新加载值。

编辑:(查看评论,我忘了你可以用标志来做到这一点)

回答新问题:

从编译器的角度来看,将这两个 ucomiss 组合起来并不是一个完全明显的优化。

为了组合它们,编译器必须:

  1. 认识到 ucomiss %xmm0, %xmm1ucomiss %xmm1, %xmm0“相同”。
  2. 然后它必须做一个普通的子表达式消除过程才能把它拉出来。

所有这些都需要在编译器进行指令选择后完成。大多数优化过程都是在指令选择之前完成的。

更让我担心的是,为什么在您摆脱了 volatiles 之后,f1f2 没有保存在寄存器中。 -O3 真的给了你这个吗?

关于c - GCC x86-64 次优汇编输出,为什么?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/7465696/

相关文章:

python - 如何通过python脚本获取C运行程序的gtk窗口?

C 到 MIPS 的转换。数组加载字偏移量?

c++ - C 头文件中的函数声明与其在 .cpp 文件中的定义之间的一致性

c - 当最大天数小于到期前警告天数时,pam_acct_mgmt 出现段错误

将特定数组元素转换为整数值

optimization - 使用 -g 编译会导致代码变慢吗?

c - macOS:找不到 -lpaho-mqtt3c 的库

linux - 在 Linux 上构建 FBX SDK

c - 为什么 ICC 在 x86 上的汇编中生成 "inc"而不是 "add"?

assembly - 关于在 Fasm 程序集中声明和初始化结构