首页 文章

<快于<=?

提问于
浏览
1421

我正在读一本书,作者说 if( a < 901 )if( a <= 900 ) 快 .

与此简单示例不完全相同,但循环复杂代码略有性能变化 . 我想这必须对生成的机器代码做一些事情,如果它甚至是真的 .

13 回答

  • 1600

    不,它在大多数架构上都不会更快 . 您没有指定,但在x86上,所有的整数比较通常都会在两个机器指令中实现:

    • testcmp 指令,设置 EFLAGS

    • Jcc (jump) instruction,具体取决于比较类型(和代码布局):

    • jne - 如果不相等则跳转 - > ZF = 0

    • jz - 如果为零(等于)则跳转 - > ZF = 1

    • jg - 如果更大则跳转 - > ZF = 0 and SF = OF

    • (等......)


    Example (编辑简洁)用 $ gcc -m32 -S -masm=intel test.c 编译

    if (a < b) {
            // Do something 1
        }
    

    编译为:

    mov     eax, DWORD PTR [esp+24]      ; a
        cmp     eax, DWORD PTR [esp+28]      ; b
        jge     .L2                          ; jump if a is >= b
        ; Do something 1
    .L2:
    

    if (a <= b) {
            // Do something 2
        }
    

    编译为:

    mov     eax, DWORD PTR [esp+24]      ; a
        cmp     eax, DWORD PTR [esp+28]      ; b
        jg      .L5                          ; jump if a is > b
        ; Do something 2
    .L5:
    

    因此,两者之间的唯一区别是 jgjge 指令 . 这两个将花费相同的时间 .


    我可以给出的是:在Intel Instruction Set Reference中,它们都在一个共同的指令下组合在一起, Jcc (如果满足条件则跳转) . 附录C中的相同分组在Optimization Reference Manual下进行 . 延迟和吞吐量 .

    Latency - 执行内核完成执行构成指令的所有μop所需的时钟周期数 . 吞吐量 - 在发出端口可以再次接受相同指令之前等待所需的时钟周期数 . 对于许多指令,指令的吞吐量可能远远小于其延迟

    Jcc 的值为:

    Latency   Throughput
    Jcc     N/A        0.5
    

    Jcc 上有以下脚注:

    7)条件跳转指令的选择应基于第3.4.1节“分支预测优化”一节的建议,以提高分支的可预测性 . 当成功预测分支时,jcc的延迟实际上为零 .

    因此,英特尔文档中的任何内容都没有与其他文档区别对待任何一条 Jcc 指令 .

    如果考虑用于实现指令的实际电路,可以假设在 EFLAGS 中的不同位上将存在简单的AND / OR门,以确定是否满足条件 . 那么,没有理由测试两位的指令应该比仅测试一位的指令花费更多或更少的时间(忽略门传播延迟,这远远小于时钟周期) .


    Edit: Floating Point

    这也适用于x87浮点数:(与上面的代码完全相同,但使用 double 而不是 int . )

    fld     QWORD PTR [esp+32]
            fld     QWORD PTR [esp+40]
            fucomip st, st(1)              ; Compare ST(0) and ST(1), and set CF, PF, ZF in EFLAGS
            fstp    st(0)
            seta    al                     ; Set al if above (CF=0 and ZF=0).
            test    al, al
            je      .L2
            ; Do something 1
    .L2:
    
            fld     QWORD PTR [esp+32]
            fld     QWORD PTR [esp+40]
            fucomip st, st(1)              ; (same thing as above)
            fstp    st(0)
            setae   al                     ; Set al if above or equal (CF=0).
            test    al, al
            je      .L5
            ; Do something 2
    .L5:
            leave
            ret
    
  • 30

    从历史上看(我们说的是20世纪80年代和90年代初),有些架构就是这样 . 根本问题是整数比较通过整数减法固有地实现 . 这引起了以下情况 .

    Comparison     Subtraction
    ----------     -----------
    A < B      --> A - B < 0
    A = B      --> A - B = 0
    A > B      --> A - B > 0
    

    现在,当 A < B 减法必须借用一个高位来使减法正确时,就像你在手动添加和减去时携带和借用一样 . 这个"borrowed"位通常被称为进位,并且可以通过分支指令进行测试 . 如果减法相同为零,则表示相等,则将设置称为零位的第二位 .

    通常至少有两个条件分支指令,一个用于分支进位,另一个用于零位 .

    现在,为了解决问题的核心,让我们扩展前一个表以包含进位和零位结果 .

    Comparison     Subtraction  Carry Bit  Zero Bit
    ----------     -----------  ---------  --------
    A < B      --> A - B < 0    0          0
    A = B      --> A - B = 0    1          1
    A > B      --> A - B > 0    1          0
    

    因此,为 A < B 实现一个分支可以在一条指令中完成,因为只有在这种情况下进位才清楚,即

    ;; Implementation of "if (A < B) goto address;"
    cmp  A, B          ;; compare A to B
    bcz  address       ;; Branch if Carry is Zero to the new address
    

    但是,如果我们想要进行小于或等于比较,我们需要对零标志进行额外检查以捕获相等的情况 .

    ;; Implementation of "if (A <= B) goto address;"
    cmp A, B           ;; compare A to B
    bcz address        ;; branch if A < B
    bzs address        ;; also, Branch if the Zero bit is Set
    

    因此,在某些机器上,使用"less than"比较可能会保存一条机器指令 . 这与亚兆赫处理器速度和1:1 CPU到内存速度比的时代相关,但它现在几乎完全无关紧要 .

  • 49

    假设我们正在谈论内部整数类型,那么没有可能比另一个更快 . 它们在语义上显然是相同的 . 他们都要求编译器做同样的事情 . 只有一个可怕的破坏编译器会为其中一个生成劣质代码 .

    如果某个平台的 <<= 快于简单整数类型,则编译器应始终将 <= 转换为 < 作为常量 . 任何编译器都不是一个糟糕的编译器(对于那个平台) .

  • 13

    我发现两者都不快 . 编译器在每个条件下使用不同的值生成相同的机器代码 .

    if(a < 901)
    cmpl  $900, -4(%rbp)
    jg .L2
    
    if(a <=901)
    cmpl  $901, -4(%rbp)
    jg .L3
    

    我的例子 if 来自Linux上x86_64平台上的GCC .

    编译器编写者非常聪明,他们会想到这些事情我们大多数人认为理所当然 .

    我注意到如果它不是常数,那么在任何一种情况下都会生成相同的机器代码 .

    int b;
    if(a < b)
    cmpl  -4(%rbp), %eax
    jge   .L2
    
    if(a <=b)
    cmpl  -4(%rbp), %eax
    jg .L3
    
  • 6

    对于浮点代码,即使在现代架构上,<=比较确实可能更慢(通过一条指令) . 这是第一个功能:

    int compare_strict(double a, double b) { return a < b; }
    

    在PowerPC上,首先执行浮点比较(更新 cr ,条件寄存器),然后将条件寄存器移到GPR,将"compared less than"位移位,然后返回 . 它需要四条指令 .

    现在考虑这个功能:

    int compare_loose(double a, double b) { return a <= b; }
    

    这需要与上面的 compare_strict 相同的工作,但现在有两个感兴趣的位:"was less than"和"was equal to."这需要一个额外的指令( cror - 条件寄存器按位OR)将这两个位组合成一个 . 因此 compare_loose 需要五条指令,而 compare_strict 需要四条指令 .

    您可能认为编译器可以像这样优化第二个函数:

    int compare_loose(double a, double b) { return ! (a > b); }
    

    但是这会错误地处理NaN . NaN1 <= NaN2NaN1 > NaN2 都需要评估为false .

  • 34

    也许这个未命名的书的作者已经读到 a > 0a >= 1 跑得更快,并认为这是普遍的 .

    但这是因为涉及 0 (因为 CMP 可以,取决于架构,例如用 OR 替换)而不是因为 < .

  • 11

    至少,如果这是真的,编译器可以轻而易举地优化<= b到!(a> b),所以即使比较本身实际上更慢,除了最天真的编译器之外,你不会注意到差别 .

  • 573

    他们有相同的速度 . 也许在一些特殊的架构中他/她说的是对的,但至少在x86家族中我知道它们是相同的 . 因为为此,CPU将进行减法(a-b),然后检查标志寄存器的标志 . 该寄存器的两位称为ZF(零标志)和SF(符号标志),它在一个周期内完成,因为它将通过一个屏蔽操作完成 .

  • 66

    这将高度依赖于C编译的底层架构 . 某些处理器和体系结构可能具有等于或小于等于在不同循环数中执行的明确指令 .

    这可能是非常不寻常的,因为编译器可以解决它,使其无关紧要 .

  • 15

    TL; DR回答

    对于大多数架构,编译器和语言的组合,它不会更快 .

    完整答案

    其他答案集中在x86体系结构上,我不知道ARM体系结构(你的示例汇编器似乎是这样)足以对生成的代码做出具体评论,但这是micro-optimisation的一个例子,它非常特定于体系结构,并且是 as likely to be an anti-optimisation as it is to be an optimisation .

    因此,我建议这种micro-optimisationcargo cult编程的一个例子,而不是最好的软件工程实践 .

    可能有一些架构,这是一个优化,但我知道至少有一个架构,相反可能是真的 . 古老的Transputer架构只有等于或大于或等于的机器代码指令,因此必须从这些原语构建所有比较 .

    即便如此,在几乎所有情况下,编译器都可以按照这样的方式对评估指令进行排序:在实践中,没有任何比较比任何其他比较都有任何优势 . 最糟糕的情况是,它可能需要添加反向指令(REV)来交换operand stack上的前两项 . 这是一个单字节指令,需要一个周期才能运行,所以可能的开销最小 .

    这样的微优化是优化还是反优化取决于您使用的特定体系结构,因此养成使用体系结构特定微优化的习惯通常是个坏主意,否则您可能会本能地当它不合适时使用一个,看起来这正是你正在阅读的书所倡导的 .

  • -6

    即使有任何差异,您也不应该注意到差异 . 此外,在实践中,你必须做一个额外的 a + 1a - 1 来使条件成立,除非你要使用一些魔法常数,这是一种非常糟糕的做法 .

  • 85

    您可以说大多数脚本语言中的行是正确的,因为额外的字符会导致代码处理稍慢 . 但是,正如最顶层的回答所指出的那样,它应该对C没有任何影响,并且使用脚本语言完成的任何事情可能都不关心优化 .

  • 3

    实际上,它们的速度完全相同,因为在装配级别它们都采用一条线 . 如:

    • jl ax,dx (如果AX小于DX则跳转)

    • jle ax,dx (如果AX小于或等于DX,则跳转)

    所以不,也不是更快 . 但是,如果你想要真正的技术,我想如果你要在电子电流水平检查它,它会稍微快一点,但不会接近你会注意到的速度 .

相关问题