是<比<=更快?

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

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

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

  • testcmp指令,设置EFLAGS
  • 和一个Jcc (跳转)指令 ,取决于比较types(和代码布局):
    • 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)条件跳转指令的select应基于第3.4.1节“分支预测优化”的build议,以提高分支的可预测性。 当分支预测成功时, jcc的延迟实际上是零。

因此,英特尔文档中没有任何内容与其他Jcc指令有任何区别。

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


编辑:浮点

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

历史上(我们正在谈论20世纪80年代和90年代初),有一些架构,这是真实的。 根本问题是整数比较本质上是通过整数相减来实现的。 这引起了以下情况。

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

现在,当A < B ,减法必须借用高位来进行减法才是正确的,就像手工加减时的携带和借用一样。 这个“借位”位通常被称为进位位 ,并且可以通过分支指令进行testing。 如果相减为相同的零,则称为零位的第二位将被设置,这意味着相等。

通常至less有两个条件分支指令,一个在进位位上分支,另一个在零位上。

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

 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于”比较可能会节省一条机器指令 。 这在处理器速度超过兆赫兹,处理器速度为1:1的时代是相关的,但现在几乎完全不相关。

假设我们正在谈论内部整数types,没有办法可以比另一个更快。 他们在语义上显然是相同的。 他们都要求编译器做同样的事情。 只有一个可怕的破碎的编译器会为其中的一个产生较差的代码。

如果有一些平台的<<=简单的整数types更快,编译器应该始终<=转换为<常量。 任何编译器不会只是一个糟糕的编译器(为该平台)。

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

 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 

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

 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需要评估为假。

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

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

至less,如果这是真的,编译器可以优化一个<= b to!(a> b),所以即使比较本身实际上比较慢,除了最天真的编译器,你也不会注意到有什么区别。

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

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

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

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

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

这可能是一些优化的体系结构,但我知道至less有一种架构可能是正确的。 历史悠久的Transputer体系结构只有机器代码指令等于大于或等于 ,所以所有的比较必须从这些原语中构build。

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

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

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

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

其实他们的速度也是一样的,因为在assembly层面他们都是一条线。 如:

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

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

Interesting Posts