c++ - csdn - gtags使用




是<快于<=? (9)

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

与这个简单的例子不完全一样,但是在复杂的循环代码上有轻微的性能变化。 我想这必须用生成的机器代码来做,以防万一。


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

  • testcmp指令,设置EFLAGS
  • 和一个Jcc (跳转)指令 ,取决于比较类型(和代码布局):
    • jne - 如果不相等则跳转 - > ZF = 0
    • jz - 如果零(等于) - > ZF = 1跳转
    • jg - 如果更大则跳转 - > ZF = 0 and SF = OF
    • (等等...)

示例 (为简洁起见)编译时使用$ 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指令。 这两个将花费相同的时间。

我想解决这个评论,即没有任何内容表明不同的跳转指令需要花费相同的时间。 这是一个有点棘手的答案,但这里是我可以给:在英特尔指令集参考 ,他们都在一个共同的指令, Jcc (如果条件满足跳转)组合在一起。 在“ 优化参考手册 ”的附录C“延迟时间和吞吐量”下进行相同的分组。

延迟 - 执行内核完成执行构成指令的所有μops所需的时钟周期数。

吞吐量 - 在发布端口可以再次自由接受相同指令之前等待所需的时钟周期数。 对于许多指令而言,指令的吞吐量可能远远低于其延迟

Jcc的值是:

      Latency   Throughput
Jcc     N/A        0.5

Jcc上有以下脚注:

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

因此,英特尔文档中的任何内容都不会与其他Jcc指令有所不同。

如果考虑用于实现指令的实际电路,可以假设在EFLAGS的不同位上会有简单的AND / OR门,以确定条件是否满足。 那么没有理由说测试两位的指令所花的时间或多或少比只测试一个指令的时间少(忽略门传播延迟,这比时钟周期要小得多)。

编辑:浮点

这也适用于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

也许这本未命名的书的作者已经读过, a > 0运行速度超过a >= 1并且认为这是普遍的。

但是这是因为涉及0 (因为CMP可以取决于体系结构,例如用OR取代)而不是因为<


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


你可以说在大多数脚本语言中这一行是正确的,因为额外的字符会导致代码处理速度稍慢。 但是,正如最佳答案指出的那样,它在C ++中应该没有任何作用,并且任何使用脚本语言完成的事情可能都不是关心优化。


其他答案都集中在x86体系结构上,我不知道ARM体系结构(您的示例汇编程序似乎是)足以对所生成的代码进行具体评论,但这是micro-optimisation一个示例,它非常适合体系结构具体而言,并且很可能是一种反优化,因为它是一种优化

因此,我认为这种micro-optimisation货物崇拜编程的一个例子,而不是最好的软件工程实践。

这可能是一些优化的架构,但我知道至少有一种架构可能是正确的。 历史悠久的Transputer体系结构只有机器代码指令等于大于或等于 ,因此所有比较必须从这些基元构建而成。

即使在那时,在几乎所有情况下,编译器都可以按照这样的方式排列评估指令,实际上,没有任何比较有任何优势。 最糟糕的情况是,它可能需要添加一个反向指令(REV)来交换操作数堆栈中的前两项。 这是一个单字节指令,需要一个周期才能运行,所以可能有最小的开销。

这样的微型优化是一种优化还是一种反优化取决于你正在使用的特定架构,所以习惯使用体系结构特定的微观优化通常是一个坏主意,否则你可能本能地如果这样做不恰当,就使用一个,看起来这就是你正在阅读的书正在倡导的。


即使有任何差异,你也不应该注意到这种差异。 此外,在实践中,除非你打算使用一些魔术常数,否则你必须做a + 1a + 1的附加条件,这是非常糟糕的做法。


对于浮点代码,甚至在现代架构上,<=比较的确可能会更慢(只需一条指令)。 这是第一个功能:

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

在PowerPC上,首先执行浮点比较(更新cr ,条件寄存器),然后将条件寄存器移至GPR,将“比较小于”位移入原位,然后返回。 它需要四条指令。

现在考虑这个函数:

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

这需要与上面的compare_strict相同的工作,但现在有两点兴趣:“小于”和“等于”。 这需要一个额外的指令( cror - 条件寄存器按位或)将这两个位合并为一个。 所以compare_loose需要5条指令,而compare_strict需要4条。

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

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

但是,这将错误地处理NaN。 NaN1 <= NaN2NaN1 > NaN2需要评估为假。


我发现这两者都不是更快。 编译器在每个条件下生成具有不同值的相同机器代码。

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

这将高度依赖C编译的底层架构。 一些处理器和体系结构可能具有等于或小于等于的明确指令,其以不同数目的周期执行。

这将是非常不寻常的,因为编译器可以解决它,使它不相关。





relational-operators