【问题标题】:GCC x86-64 Suboptimal Assembly Output, why?GCC x86-64 次优汇编输出,为什么?
【发布时间】:2011-11-19 21:50:57
【问题描述】:

查看以下代码的汇编输出时(没有优化,-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

最终更新

在查看了一些后续 cmets 后,han(在 Jonathan Leffler 的帖子下发表评论)似乎解决了这个问题。 GCC 不进行优化不是因为它不能,而是因为我没有告诉它。似乎这一切都归结为 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

请注意,两个 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 或SNaN。”

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

【问题讨论】:

  • 有效吗?不,提出一个错误。是的,接受 GCC 开发人员在优化方面比绝大多数人更聪明的事实。 :-)
  • 我认为这就是volatile 限定符的全部意义所在。删除它并进行比较。
  • 我在没有 volatile 的情况下更新了我的结果。我最初放入 volatile 是为了从 scanf() 调用中清理代码。
  • “用scanf替换它”是什么意思?向我们展示 C 代码...
  • @paxdiablo,你看到了什么错误。我已经运行了组装代码,很好地测试了 xmm0 和 xmm1 的多个值,它按我的预期工作。

标签: c gcc assembly x86-64


【解决方案1】:

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

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

编辑:(见 cmets,我忘了你可以用标志做到这一点)

回答新问题:

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

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

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

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

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

【讨论】:

  • 您不需要进行两次比较 - 您可以对第一个进行 jg(ZF=0 和 SF=OF),对第二个进行 jl(SF != OF)跨度>
  • @bdonlan:很好,我忘了你可以用标志来做到这一点。
  • @bdonlan,所以你是说在我的修改后的代码中应该用 jg 替换 ja 以便它使用签名比较?您能否发布一个正式的答案来回答重复的 movss 和 ucomiss 是否是冗余的以及非冗余代码应该是什么样的?谢谢!
  • @Mystical,我发布的代码是使用没有优化标志生成的。当您使用 O3 进行优化时,您会删除重复的 MOVSS,但它仍然会输出两个 UCOMISS,颠倒 xmm 的顺序并使用 ja 测试两个结果。所以 O3 更好,但是如果没有看到小于或大于可以通过一次呼叫 UCOMISS 然后是 ja 和 jb 来测试(我希望,我想这是我在最初的问题之后寻找的人来确认的)。
  • @Brandon:那么我认为这个问题的答案是 GCC 目前无法进行这种优化,因为它非常本地化。 (换句话说,你已经超越了 GCC)我更新的答案解释了为什么编译器不可能进行这种优化。 (但需要一些编译器知识才能理解)
【解决方案2】:

volatile 限定符意味着f1f2 的值可能会以编译器无法检测/预期的方式发生变化,因此它必须在每次使用f1f2 时访问内存。生成的代码就是这样做的——所以它是正确的。

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


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

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

ucomiss 指令的操作数的顺序在颠倒的条件下被颠倒:

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

我不相信优化器会尽可能优化,但问题正在超出我的专业水平。

【讨论】:

  • 请查看包含 scanf() 的更新代码,它会产生与我担心的相同的重复指令。我确实看到了关于 volatile 的所有要点,但这并不是我真正想要的答案,因为 volatile 只是设法阻止编译器优化代码。
  • @Jonathan Leffler:查看我未删除帖子的评论。您可以使用标志来摆脱一个比较。 (因此我最初的回答是 dv)
  • 我想我不明白为什么跳转不会简单地依赖于在第一条 UCOMISS 指令之后设置的 ZF、PF 和 CF 标志?如果 ZF 或 CF 等于 1(小于或等于),然后运行相同的动作并再次以相反的顺序比较,通过测试 ZF = 1(等于to) 和 CF = 0 (大于)。剩下的唯一情况是 ZF 和 CF 都为 0(小于)。
  • volatile 和原来的一样,编译器的手被束缚了;它必须每次访问变量两次。如果没有volatile,则不清楚优化器是否正在进行人类可能针对同一任务进行的所有优化。不过,可能有一些关于指令调度等我不知道的东西。
  • @Brandon 你考虑过无序结果的情况吗?如果任一操作数为 NaN,则 C 代码中的两个比较都必须返回 false。我很确定这就是 GCC 使用 JA 进行两种比较的原因。然而,-ffast-math 选项应该消除双重比较的这个原因,但它对 GCC 4.6.0 的唯一影响是从 UCOMISS 切换到 COMISS。也许这只是一个错过的优化。
猜你喜欢
  • 2012-09-17
  • 2019-05-24
  • 2010-11-21
  • 2022-10-15
  • 2011-08-25
  • 1970-01-01
  • 2014-01-16
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多