首页 文章

GCC x86-64装配输出欠佳,为什么?

提问于
浏览
8

查看以下代码的程序集输出时(无优化,-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;
}

海湾合作委员会做了一些我很难遵循的事情:

.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

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


Update

如果你删除我最初使用的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

Final Update

在回顾了一些后续评论之后,似乎是汉(根据Jonathan Leffler的帖子评论过)解决了这个问题 . 海湾合作委员会不进行优化不是因为它不能,而是因为我没有告诉它 . 似乎这一切都归结为IEEE浮点规则并且要处理严格的条件GCC不能简单地在第一个UCOMISS之后跳转或跳跃,因为它需要处理浮点数的所有特殊条件 . 当使用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

请注意,现在用一个COMISS直接替换两个UCOMISS指令,然后是JA(如果上面是跳转)和JB(如果下面跳转) . 如果你让它使用-ffast-math,GCC能够确定这个优化!

UCOMISS vs 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或SNaN,则COMISS指令信号无效 . ”

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

2 回答

  • 3

    这是另一个原因:如果仔细观察它,它就不是同一个表达式 .

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

    编辑:(见评论,我忘了你可以用旗帜做到这一点)

    回答新问题:

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

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

    • 认识 ucomiss %xmm0, %xmm1 是"same"为 ucomiss %xmm1, %xmm0 .

    • 然后它必须执行一个公共的子表达式消除传递以将其拉出 .

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

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

  • 4

    volatile 限定符意味着 f1f2 的值可能会以编译器无法检测/期望的方式更改,因此每次使用 f1f2 时它都必须访问内存 . 生成的代码就是这样 - 所以它是正确的 .

    如果从任一变量或两个变量中删除 volatile 限定符,则与您获得的代码进行比较和对比 . 最终,您可能需要从某处读取 f1f2 的值,以避免编译器在编译时评估表达式 .


    在更新的代码中, ucomiss 指令有两种不同的咒语,尽管前面的 movss 指令是相同的:

    ucomiss %xmm0, %xmm1
        ucomiss %xmm1, %xmm0
    

    对于反转条件, ucomiss 指令的操作数顺序相反:

    if (f1 > f2)
    if (f1 < f2)
    

    我不相信优化器正在尽可能优化,但问题是变形超出了我的专业水平 .

相关问题