我在读一本书,作者说if( a < 901 )比if( a <= 900 )快。
与这个简单的例子不完全一样,但是循环复杂代码的性能会有轻微的变化。我想这与生成的机器代码有关,以防它是真的。
- 考虑到这个问题的历史意义、答案的质量以及其他表现最佳的问题仍然是开放的,我看不出为什么这个问题应该关闭(尤其是不删除,因为投票目前正在显示)。最多应该锁上。此外,即使问题本身是错误的/幼稚的,事实上它出现在一本书中意味着原始的错误信息存在于某处的"可信"来源,因此这个问题是建设性的,因为它有助于澄清这一点。
- 你从来没有告诉我们你指的是哪本书。
- 键入<比键入<=快两倍。
- 这是一个非常好的问题,并且对它如何涉及解释语言(如python)感兴趣。是否考虑发布一个新问题,例如"在python中>比>=快?"但这可能被视为一个重复的问题。欢迎指导。
- 8086年是真的。
- 上票的数量清楚地表明,有数百人严重过度优化。
不,在大多数体系结构上它不会更快。您没有指定,但是在x86上,所有的积分比较通常在两个机器指令中实现:
- 设置EFLAGS的test或cmp指令。
- 以及一条Jcc指令(跳转),具体取决于比较类型(和代码布局):
- jne—不等于时跳转——>ZF = 0。
- jz—如果为零(等于)->ZF = 1,则跳转。
- jg—如果大于该值,则跳转--->ZF = 0 and SF = OF。
- (等)
示例(为简洁而编辑)用$ gcc -m32 -S -masm=intel test.c编译
1 2 3
| if (a < b) {
// Do something 1
} |
编译到:
1 2 3 4 5
| 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: |
和
1 2 3
| if (a <= b) {
// Do something 2
} |
编译到:
1 2 3 4 5
| 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: |
因此,两者之间的唯一区别是jg指令与jge指令。这两个都需要同样的时间。
我想说明的是,没有任何东西表明不同的跳转指令需要相同的时间。这是一个有点难回答的问题,但我可以给出以下结论:在Intel指令集引用中,它们都在一个公共指令Jcc下分组(如果满足条件,则跳转)。同样的分组是在优化参考手册的附录C.延迟和吞吐量下进行的。
Latency — The number of clock cycles that are required for the
execution core to complete the execution of all of the μops that form
an instruction.
Throughput — The number of clock cycles required to
wait before the issue ports are free to accept the same instruction
again. For many instructions, the throughput of an instruction can be
significantly less than its latency
Jcc的值为:
1 2
| Latency Throughput
Jcc N/A 0.5 |
在Jcc上有以下脚注:
7) Selection of conditional jump instructions should be based on the recommendation of section Section 3.4.1,"Branch Prediction Optimization," to improve the predictability of branches. When branches are predicted successfully, the latency of jcc is effectively zero.
因此,英特尔文档中的任何内容都不会将一条Jcc指令与其他指令区别对待。
如果考虑到用于执行指令的实际电路,可以假设EFLAGS中不同位上有简单的和/或门,以确定是否满足条件。因此,测试两个位的指令所花费的时间不应超过或少于一个测试所花费的时间(忽略比时钟周期短得多的门传播延迟)。
编辑:浮点
这同样适用于x87浮点:(与上面的代码基本相同,但使用double而不是int)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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 |
- @Dyppl实际上jg和jnle是同一条指令,7F—)
- @乔纳森·莱因哈特,你确定你的例子不是相反的吗?也就是说,<不是编译成jg,<=不是编译成jge?
- @maksimov可能是正确的,(a < b) ...的asm代码说:jump if a >= b,相当于do something if a < b。
- 更不用说,如果一个选项确实比另一个更快,优化器可以修改代码。
- 仅仅因为某些东西会产生相同数量的指令,并不一定意味着执行所有这些指令的总时间之和是相同的。实际上,更多的指令可以更快地执行。每个周期的指令不是固定的数字,它取决于指令。
- @我很清楚这一点。你读过我的答案吗?我没有说明任何关于相同数量的指令,我说它们被编译成本质上完全相同的指令,除了一个跳转指令正在查看一个标志,而另一个跳转指令正在查看两个标志。我相信我已经提供了足够的证据来证明它们在语义上是相同的。
- 是的,现在看到了。我仍然认为你的第一句话会导致有人出于错误的原因得出结论。"您没有指定,但是在x86上,所有的积分比较通常在两个机器指令中实现,"这实际上不是您应该做的主要点,但它是您做的第一个点。"你得把你编辑的部分往下看,才能明白为什么。否则你的答案是一流的!
- "如果考虑到用于实现指令的实际电路,可以假设eflags中不同位上有简单的和/或门,以确定是否满足条件。因此,测试两个位的指令所花费的时间不应超过或少于一个测试所花费的时间(忽略了比时钟周期短得多的门传播延迟)。"我认为这应该是您的要点。
- @你说得很好。对于这个答案所能得到的可视性,我应该对它进行一点清理。感谢您的反馈。
- 我想补充一下,cmp设置FLAGS寄存器的方式与sub指令的方式相同。实际上,"比较是通过从第一个操作数中减去第二个操作数来执行的"——因此涉及到进位/借用传播。也就是说,就硬件"并行性"而言,这不是一个简单的位操作。
- @确实如此,但是JCC指令测试已经设置的位。你的观点是正确的,但我不知道它如何真正适用于手头的问题。
- "这同样适用于x87浮点型"这是我从未听说过的新架构吗?;)
- @jonathonreinhart:在x86中,一些指令设置了一些标志,但保持其他的不变(例如inc/dec)。当前的无序执行CPU分别重命名标志位,因此inc不依赖于标志的前一个值。依赖多个指令设置的多个标志的jcc需要一个额外的UOP来合并标志(或在早期的Intel设计中,会导致部分标志暂停),因此每个jcc在内部是相同的,但它们的不同依赖性可能是一个问题。在标记重命名改善之前,情况更糟。
- @JonathonReinhart:另外,请参阅agner.org/optimize,了解比英特尔自己的手册更详细的信息。
- 上次忘了提这个,但并不是每个JCC都是一样的。有些可以在core2和nehalem上使用前面的CMP或测试指令进行宏融合。(在Intel SandyBridge系列上,有许多不同的ALU指令。)AMD的CPU完全可以进行宏融合(推土机系列),可以对任何JCC进行宏融合,甚至像JP这样的奇怪的CPU,Intel从未进行宏融合。
- @彼得卡兹,自从我写了这个答案,我参加了一个研究生级别的计算机体系结构课程,并且对流水线和寄存器重命名等复杂问题有了更多的了解。我仍然坚信我的答案(基本上就是"否")是正确的,但我不太确定在我的答案中添加什么。从现代无序超标量CPU的角度来看,这是正确的。也许简单的答案是"无论底层机器如何,硬件都能够同时查看多个条件标志"。有什么想法吗?
- 是的,不是在eflags中对多个位的实际测试才是x86的问题。它是部分标志重命名,因为不是所有的指令都写每个标志,但是CPU试图通过分别重命名eflags的不同部分来避免错误的依赖性。(这不是
- 不管怎样,我的评论只是试图纠正所有JCC都是平等的过度概括。它们不是这样的,因为有些可以宏融合,而有些则不能,即使是在CMP这样的编写所有标志的指令之后使用(避免部分标志重命名暂停或减速)。
从历史上看(我们讲的是20世纪80年代和90年代初),有些架构是这样的。根本问题是整数比较是通过整数减法固有地实现的。这会导致以下情况。
1 2 3 4 5
| Comparison Subtraction
---------- -----------
A < B --> A - B < 0
A = B --> A - B = 0
A > B --> A - B > 0 |
现在,当A < B时,减法必须借用一个高位才能使减法正确,就像用手加减时进行进位和借位一样。这个"借用"位通常被称为进位,可以通过分支指令进行测试。如果减法等于零,则将设置第二个名为零的位,这意味着相等。
通常至少有两个条件分支指令,一个在进位上分支,一个在零位上分支。
现在,为了弄清问题的核心,我们将上一个表扩展到包含进位和零位结果。
1 2 3 4 5
| 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实现分支可以在一条指令中完成,因为进位只有在这种情况下才是清晰的,也就是说,
1 2 3
| ;; 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 |
但是,如果我们想做一个小于或等于的比较,我们需要对零标志做一个额外的检查来捕捉相等的情况。
1 2 3 4
| ;; 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 |
因此,在某些机器上,使用"小于"比较可能会节省一条机器指令。这在亚兆赫处理器速度和1:1 CPU对内存的速度比的时代是相关的,但今天几乎完全不相关。
- 此外,像x86这样的架构实现了诸如jge之类的指令,这些指令测试零和符号/进位标志。
- 以东十一〔13〕为历史视角。
- 应该注意的是,在一个过程/函数/程序的计算循环中,一条附加的指令可能会产生差异。正如@greyfade所提到的,与速度更相关的是,大多数现代的cisc处理器都有检查进位和零标志的跳转/分支指令,因此仍然只使用一条指令。
- 即使对于给定的体系结构来说是真的。没有一个编译器编写者注意到的可能性有多大,并且添加了一个优化以用更快的速度替换较慢的速度?
- 这在8080年代是真的。它有跳转到零和跳转到负的指令,但没有一个可以同时测试两者。
- 这也是6502和65816处理器系列的情况,它也扩展到了摩托罗拉68HC11/12。
- @乔汉娜:这是优化版。对于循环,分支if equal指令只在循环的最后一次迭代中遇到,因此它的影响被分摊到循环的某一部分。颠倒测试需要在内部循环中放置额外的指令,这将影响每个循环迭代。此外,可能不可能颠倒比较顺序,因为这些通常是累加器架构,将累加器溢出到内存中的开销要比添加额外的条件分支指令的开销大得多。
- 卢卡斯:@jon可能指的是A < (B + 1)优化,如果b是一个常数。
- +这是唯一解释为什么作者可能写了他所做的事情的答案。
- 我喜欢这个答案,因为它让我想起了6502的乐趣,也让我想起了当我搬到C的时候,我有多么怀念国旗。它也证明了这个问题比大多数人所认为的更深刻、更有趣。
- 即使在8080上,一个<=测试也可以在一条指令中实现,交换操作数,测试not <(相当于>=),这就是交换操作数所需的<=:cmp B,A; bcs addr。这就是Intel省略此测试的原因,他们认为它是多余的,在那些时候您无法负担多余的指令:—)
- 我很确定其中一些架构仍在嵌入式使用中,所以即使它们诞生于80年代,它们也不一定死在那里。
- @你是绝对正确的。我不相信在任何架构和场景中都需要这个双重测试。
- @乔纳森莱哈特,你基本上是对的。即使在80年代,窥视孔优化器也会颠倒比较,或者重新排序if/else代码分支,以消除额外的测试。但是一个幼稚的编译器或者缺乏经验的汇编语言程序员可能仍然会产生这样的输出。
- @卢卡斯:事实上,6502年(65816年)的情况并非如此。6502在这种情况下有两个感兴趣的分支比较指令:BCC和BCS。BCC的工作方式类似于>,而BCS的工作方式类似于<。例如,lda$01:cmp$02:bcs label implements<。如果需要<=,可以简单地交换参数-lda$02:cmp$01:bcc label
- @问题:是的,6502有一个进位标志测试的否定形式,但我试图做的是需要两个单独的指令来测试进位标志(bcc/bcs)和零标志(beq/bne),因为6502没有指令来同时测试P寄存器的多个值。有了bcc/bcs对,不必更改累加器中的值就可以轻松地反转比较结果。
- @Hirschhornsalz:在8080上,反转操作数是一种标准技术,但是各种因素可能会使首先计算特定操作数更好。例如,如果给定static unsigned char x;,表达式x < 20可以计算为ld a,(x) / cmp 20 / jnc nope,但是逆转x > 20的操作数需要类似于ld a,20 / ld hl,x / cmp (hl) / jnc nope。最好是维持秩序,但用x <= 21:ld a,(x) / cmp 21 / jc nope。
- 与SuffCee一样,智能编译器可以使用各种技巧将C++比较编译成高效的ASM。如果其中一个操作数是编译时常数,它可以使asm检查x < 21,而不是x <= 20。或者在x86上,编译器可能会选择使常量的大小变小,这样它们就可以适应有符号的8位立即数而不是32位立即数。例如,用x <= 127代替x < 128。但如果这两个都是运行时变量,那么for( ... ; i < size ;)就不能保证是无限循环,但i <= size可能是(无符号)!这可以击败优化。
假设我们讨论的是内部整数类型,那么没有可能一种方法比另一种方法更快。它们显然在语义上是相同的。它们都要求编译器做完全相同的事情。只有极坏的编译器才能为其中一个生成劣质代码。
如果某个平台的<比<=更快,那么对于简单的整数类型,编译器应该总是将<=转换为<作为常量。任何不是这样的编译器(对于那个平台)都会是一个糟糕的编译器。
- +1的同意。我不<=曼弗雷迪或法拉利吗<decides which until the编译速度速度会有。这是很简单的,当你考虑compilers optimisation for that they generally已经做死optimisation呼叫队列,尾环(optimisation展开,hoisting和在线,自动,occasions)parallelisation of 49环,等等。为什么浪费时间pondering过早的优化?get it to running a prototype,轮廓的确定在最重要的那些优化性能优化在FIPS,再沿轮廓阶of significance and the Way to measure的进步……
- there are some cases where我边缘有一个常数的值的比较是在slower<=变换,例如,从(a < C)to when the (a <= C-1)(for some等原因C)Cto be to the更难在指令集编码。for example,安指令集可能常数的表示方法signed 127~128在唱片中的形态比较,but that have to载常数的范围在使用或者在一周前,slower encoding,entirely指令。我知道(a < -127)may not have比较喜欢在straightforward变换。
- beeonrope was not the"的问题,是否有differed表演操作两个不同的常数的影响表现在他们可以用whether the same but口语的影响不同的常数可以操作的性能。我们不知道You have to a > 128?比较a > 127because there你没有选择,你需要使用一个。我们a >= 128?比较a > 127to which require,不能因为他们不同的不同的编码或教学真理have the same table。任何一equally encoding of encoding of the other is an。
- 我对您的声明做出了一般性的回应,"如果有某个平台,[<=速度较慢],编译器应该始终将<=转换为<作为常量。"据我所知,转换涉及到改变常数。例如,由于<更快,所以a <= 42被编译为a < 43。在某些边缘情况下,这样的转换将不会产生效果,因为新常量可能需要更多或更慢的指令。当然,a > 127和a >= 128是等价的,编译器应该以(相同的)最快的方式对这两种形式进行编码,但这与我所说的并不矛盾。
我看也不快。编译器在每个条件下生成具有不同值的相同机器代码。
1 2 3 4 5 6 7
| if(a < 901)
cmpl $900, -4(%rbp)
jg .L2
if(a <=901)
cmpl $901, -4(%rbp)
jg .L3 |
我的示例if来自Linux上x86_平台上的gcc。
编译器作者是相当聪明的人,他们想到这些事情,我们大多数人都认为是理所当然的。
我注意到,如果它不是一个常量,那么在这两种情况下都会生成相同的机器代码。
1 2 3 4 5 6 7 8
| int b;
if(a < b)
cmpl -4(%rbp), %eax
jge .L2
if(a <=b)
cmpl -4(%rbp), %eax
jg .L3 |
- 请注意,这是特定于x86的。
- 的确-我应该这么说-但是任何编译器都可以足够聪明地生成这段代码
- 我认为您应该使用if(a <=900)来证明它生成的ASM完全相同:)
- @Lipis抱歉-我不理解你的评论-你能澄清一下吗-我展示了两个if声明生成的ASM
- @阿德里安科尼什对不起……我编辑了它。差不多是一样的……但是,如果将第二个if更改为<=900,则asm代码将完全相同:)现在几乎相同。但你知道…对于强迫症患者:
- 啊,我明白了——对不起,我错过了操作的原始问题中的不同值——我的观点是编译器在生成的asm中编辑了该值。
- @Adriancornish你的两个陈述和问题中的不同。他的一个有900,而不是901。
- @qsario非常正确-我错过了-尽管编译器正在编辑值,但这一点仍然存在
- 那if (a <= INT_MAX)呢?
- 您是正确的,但最好在其他原始语句中进行编辑,以确保完整性:)
- @是的,你完全是对的。我们在同一页:)我编辑了它..希望你不介意……
- @博恩,可能会被减少到如果(真的)和完全消除。
- @qsario我认为这一点很难理解,因为在这种情况下,两个asm语句都变成了cmpl $900, -4(%rbp),所以很难看出区别。因为我在代码中显示的是ASM,而不是操作系统,所以这并不是错误的——而是突出显示了本书中的错误。
- 请考虑下面:typedef int a,typedef int b,a c = 1;b d = 2;if( c < d )&;if( c <= d )as c和d are different types
- viniyoshouta @?
- see the generated的想* ASM代码。老实说there are examples of很多的黑莓,我会看到specially about chars generated ASM代码
- viniyoshouta尝试@恩-你会给自己g++ --save-temps myfile.ccfor the .s文件你可以读到我自己:(ASM)for the
- 编辑@瓜拉立卑公平但很高兴你回来是想改变差分当EN集锦茶更好。-为什么-"我们是什么新鲜事programmers强迫症:-)
- 不出一只角,this has to applies比较优化的常数。我保证它将not be done this for?比较两类变量。
- jonathonreinhart but the @全同意-问题与op' S是常数。但ASM see the generated the same that is - is to the lhs除了在cmpl -4(%rbp), %eaxmoved寄存器
- "你不adriancornish showing the Whole电影。那只是出现which the sets,which is the旗,总是一样的。你仍然会有一个differnt Jccdepending on the条件指令。看到我的示例。
- "jonathonreinhart好点。edited jump to the包括声明。
- BTW,reduces of the级GCC immediates for example,因为当它可以立即从x86安在线- 128。127 instead of 4只需要1个字节。(没有永远的伤害在我求职的编译时间常数变换for the,也许除了在网上哪里有位ARM茶具更是让更多的likely一起立即encodeable as an…有兴趣尝试的摇篮和与x < 0x00f000to see if it变成x <= 0x00efff)
对于浮点代码,即使在现代体系结构上,<=比较(通过一条指令)也可能会更慢。以下是第一个函数:
1
| int compare_strict(double a, double b) { return a < b; } |
在PowerPC上,首先执行浮点比较(更新cr,条件寄存器),然后将条件寄存器移动到gpr,将"compared less than"位移动到位,然后返回。它需要四个指令。
现在考虑这个函数:
1
| int compare_loose(double a, double b) { return a <= b; } |
这需要与上面的compare_strict相同的工作,但现在有两个兴趣:"小于"和"等于"。这需要一个额外的指令(cror条件寄存器按位或)将这两个位组合成一个。因此,compare_loose需要5条指令,而compare_strict需要4条指令。
您可能认为编译器可以像这样优化第二个函数:
1
| int compare_loose(double a, double b) { return ! (a > b); } |
但是,这将错误地处理nan。NaN1 <= NaN2和NaN1 > NaN2都需要评估为假。
- 幸运的是,它在x86(x87)上并不像这样工作。fucomip设置ZF和CF。
- @JonathonReinhart:我认为你误解了PowerPC正在做什么——条件寄存器cr相当于x86上的ZF和CF标志。(虽然cr更灵活。)海报所说的是将结果移动到gpr:它在PowerPC上接受两个指令,但x86有条件移动指令。
- @Dietrichepp在我的陈述之后我想补充的是:你可以根据eflags的值立即跳转。对不起,我不明白。
- @JonathonReinhart:是的,你也可以根据cr的值立即跳转。答案不是关于跳转,这是额外指令的来源。
也许这本无名书的作者读到了a > 0比a >= 1运行得更快,并且认为这是普遍正确的。
但这是因为涉及到一个0(因为CMP可以,根据架构,替换为OR,而不是因为<。
- 当然,在"调试"构建中,但是运行速度比(a > 0)慢需要一个坏的(a >= 1)编译器,因为优化器可以将前者琐碎地转换为后者。
- @有时候,我会惊讶于优化器可以优化哪些复杂的事情,以及它不能优化哪些简单的事情。
- 事实上,它总是值得检查ASM输出中很少的函数,在这些函数中它是重要的。也就是说,上述转换非常基础,甚至在简单的编译器中执行了几十年。
至少,如果这是真的,编译器可以将a<=b优化到!(A>B),所以即使比较本身比较慢,除了最幼稚的编译器之外,其他所有编译器都不会有什么不同。
- 为什么?(a>b)是a<=b的优化版本。不是!(a>b)2次操作合一?
- @Abhisheksinge-NOT是由其他指令(je对jne作出的)
它们有相同的速度。也许在某些特殊的建筑中,他/她所说的是对的,但在x86家族中,至少我知道他们是一样的。因为这样做,CPU将做一个减法(A-B),然后检查标志寄存器的标志。该寄存器的两个位称为ZF(零标志)和SF(符号标志),它在一个周期内完成,因为它将在一个掩码操作中完成。
这将高度依赖于C被编译到的底层体系结构。一些处理器和体系结构可能具有等于或小于等于的显式指令,这些指令以不同的周期数执行。
不过,这将是非常不寻常的,因为编译器可以绕过它,使之不相关。
- 如果圆柱体有区别的话。1)无法检测到。2)任何值得一试的编译器都会在不改变代码含义的情况下从慢形式转换为快形式。因此,所植入的指令是相同的。
- 完全同意,这在任何情况下都是一个微不足道的愚蠢的区别。当然,在一本应该是平台不可知论的书中没有提到。
- @我明白了。花了我一段时间(傻我)。不,它们是无法检测到的,因为发生了许多其他事情,使得它们的测量成为不可能的。处理器暂停/缓存未命中/信号/进程交换。因此,在正常的操作系统情况下,单周期水平上的事情在物理上是无法测量的。如果你能消除测量中的所有干扰(把它放在一个有板载内存且没有操作系统的芯片上运行),那么你仍然需要担心计时器的粒度,但是理论上,如果你运行足够长的时间,你就能看到一些东西。
回答问题
对于大多数体系结构、编译器和语言的组合,它不会更快。
完整答案
其他的答案都集中在x86架构上,我不太清楚ARM架构(您的示例汇编程序似乎是这样)是否足以专门对生成的代码进行评论,但这是一个非常特定于架构的微优化的例子,而且很可能是一个反优化,因为它是一个优化的例子。在。
因此,我建议这种微观优化是Cargo Cult编程的一个例子,而不是最佳软件工程实践。
可能有一些架构是这样优化的,但我知道至少有一个架构是相反的。古老的Transputer体系结构只有等于或大于或等于的机器代码指令,因此所有比较都必须用这些原语来构建。
即使这样,在几乎所有的情况下,编译器都可以按照这样的方式来排序评估指令:在实践中,没有任何比较比任何其他的都有优势。但在最坏的情况下,它可能需要添加一个反向指令(rev)来交换操作数堆栈的前两项。这是一个单字节的指令,只需运行一个周期,因此开销尽可能小。
不管这种微观优化是优化还是反优化,都取决于你所使用的具体架构,所以养成使用特定架构的微观优化的习惯通常是个坏主意,否则当不合适的时候,你可能会本能地使用一个,看起来这是个很好的选择。确切地说,你所读的那本书是在鼓吹什么。
即使有差异,你也不应该注意到。此外,在实践中,你需要做一个额外的a + 1或a - 1,以使条件成立,除非你要使用一些魔法常数,这是一个非常糟糕的做法。
- 什么是"坏的实践?在递增或递减计数器?how你商店的位置,那么指数?
- 如果你做辐射均值?2变量的类型。当然,如果你是平凡的王setting the value for循环或东西。但如果你有X和Y = Y<is unknown,恩,我是很好optimize slower to it to X<Y + 1
- justindanielson也表示赞同."。值得一提confusing not to丑女,等。
您可以说,在大多数脚本语言中,这一行是正确的,因为额外的字符会导致代码处理稍慢。然而,正如上面的答案所指出的,它在C++中不应该有任何效果,任何使用脚本语言的事情可能都不关心优化。
- 我有点不同意。在竞争性编程中,脚本语言通常为问题提供最快的解决方案,但必须应用正确的技术(阅读:优化)才能获得正确的解决方案。
当我写这个答案的时候,我只关注标题问题,一般来说,a < 901vs.a <= 900的具体例子。许多编译器总是通过在<和<=之间转换来缩小常量的大小,例如,因为x86立即操作数的1字节编码比-128..127短。好的。对于ARM,尤其是AARCH64,能够立即编码取决于能够将一个狭窄的字段旋转到单词中的任何位置。因此,cmp w0, #0x00f000是可编码的,而cmp w0, #0x00effff可能不是。因此,与编译时常量相比,make it较小的比较规则并不总是适用于aarch64。好的。在大多数机器上的汇编语言中,对<=的比较与对<的比较具有相同的成本。无论您是在其上进行分支、将其布尔化以创建0/1整数,还是将其用作无分支选择操作(如x86 cmov)的谓词,这都适用。其他答案只解决了问题的这一部分。好的。但这个问题是关于C++运算符,输入到优化器。通常,它们都同样有效;书中的建议听起来完全是假的,因为编译器总是可以转换它们在ASM中实现的比较。但至少有一个例外,即使用<=会意外地创建编译器无法优化的内容。好的。
作为循环条件,在某些情况下,当<=停止编译器证明循环不是无限循环时,<=与<在质量上不同。这会有很大的不同,禁用自动矢量化。好的。
与有符号溢出(UB)不同,无符号溢出被定义为基2环绕。签名循环计数器通常是安全的,因为编译器根据签名溢出ub进行优化时不会发生这种情况:++i <= size最终将变为false。(每个C程序员都应该知道哪些未定义的行为)好的。
1 2 3 4
| void foo(unsigned size) {
unsigned upper_bound = size - 1; // or any calculation that could produce UINT_MAX
for(unsigned i=0 ; i <= upper_bound ; i++)
... |
编译器只能以保留所有可能的输入值的C++源(定义的和法律上可观察的)行为的方式进行优化,除了那些导致未定义行为的行为。好的。
(一个简单的EDCOX1,12)也会产生问题,但我认为计算上限是一个更实际的例子,它意外地引入了无限循环的可能性,而不需要考虑编译器必须考虑的输入。好的。
在这种情况下,size=0导致upper_bound=UINT_MAX,i <= UINT_MAX始终是正确的。因此,对于size=0,这个循环是无限的,编译器必须考虑到这一点,即使作为程序员,您可能从未打算传递size=0。如果编译器可以将这个函数内联到一个调用者中,在调用者中它可以证明size=0是不可能的,那么很好,它可以像对i < size那样进行优化。好的。
如if(!size) skip the loop;do{...}while(--size);一样,如果循环中不需要i的实际值(为什么循环总是编译成"do…while"样式(尾跳),那么优化for( i循环通常是一种有效的方法。.好的。但是这样做了虽然不能是无限的:如果用size==0输入,我们得到2^n次迭代。(迭代for循环c中的所有无符号整数可以表示包含零的所有无符号整数的循环,但如果没有进位标志(如asm中的进位标志),就不容易了。)好的。
由于循环计数器的包围是一种可能性,现代编译器通常只是"放弃",并没有那么积极地进行优化。好的。示例:从1到n的整数之和
使用无符号i <= n会破坏clang的惯用识别方法,该方法基于高斯的n * (n+1) / 2公式,以闭合形式优化sum(1 .. n)循环。好的。
1 2 3 4 5 6
| unsigned sum_1_to_n_finite(unsigned n) {
unsigned total = 0;
for (unsigned i = 0 ; i < n+1 ; ++i)
total += i;
return total;
} |
来自Godbolt编译器资源管理器clang7.0和gcc8.2的x86-64 ASM好的。
1 2 3 4 5 6 7 8 9 10 11 12 13
| # clang7.0 -O3 closed-form
cmp edi, -1 # n passed in EDI: x86-64 System V calling convention
je .LBB1_1 # if (n == UINT_MAX) return 0; // C++ loop runs 0 times
# else fall through into the closed-form calc
mov ecx, edi # zero-extend n into RCX
lea eax, [rdi - 1] # n-1
imul rax, rcx # n * (n-1) # 64-bit
shr rax # n * (n-1) / 2
add eax, edi # n + (stuff / 2) = n * (n+1) / 2 # truncated to 32-bit
ret # computed without possible overflow of the product before right shifting
.LBB1_1:
xor eax, eax
ret |
但是对于幼稚的版本,我们只是从clang得到一个哑循环。好的。
1 2 3 4 5 6
| unsigned sum_1_to_n_naive(unsigned n) {
unsigned total = 0;
for (unsigned i = 0 ; i<=n ; ++i)
total += i;
return total;
} |
1 2 3 4 5 6 7 8 9 10
| # clang7.0 -O3
sum_1_to_n(unsigned int):
xor ecx, ecx # i = 0
xor eax, eax # retval = 0
.LBB0_1: # do {
add eax, ecx # retval += i
add ecx, 1 # ++1
cmp ecx, edi
jbe .LBB0_1 # } while( i<n );
ret |
GCC也不使用封闭形式,因此循环条件的选择并不会真正影响它;它使用simd整数加法自动向量化,在xmm寄存器的元素中并行运行4个i值。好的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| #"naive" inner loop
.L3:
add eax, 1 # do {
paddd xmm0, xmm1 # vect_total_4.6, vect_vec_iv_.5
paddd xmm1, xmm2 # vect_vec_iv_.5, tmp114
cmp edx, eax # bnd.1, ivtmp.14 # bound and induction-variable tmp, I think.
ja .L3 #, # }while( n > i )
"finite" inner loop
# before the loop:
# xmm0 = 0 = totals
# xmm1 = {0,1,2,3} = i
# xmm2 = set1_epi32(4)
.L13: # do {
add eax, 1 # i++
paddd xmm0, xmm1 # total[0..3] += i[0..3]
paddd xmm1, xmm2 # i[0..3] += 4
cmp eax, edx
jne .L13 # }while( i != upper_limit );
then horizontal sum xmm0
and peeled cleanup for the last n%3 iterations, or something. |
它还有一个普通的标量循环,我认为它用于非常小的n,和/或用于无限循环情况。好的。
顺便说一句,这两个循环在循环开销上都浪费了一条指令(以及SandyBridge系列CPU上的UOP)。用sub eax,1/jnz代替add eax,1/cmp/jcc更有效。1 UOP而不是2(SUB/JCC或CMP/JCC宏观融合后)。两个循环之后的代码无条件地写入EAX,因此它不使用循环计数器的最终值。好的。好啊。
- 很好的人为例子。关于使用eflags可能对无序执行产生的影响,您的其他意见如何?这纯粹是理论上的,还是说JB比JBE能带来更好的管道呢?
- @鲁斯蒂克斯:我有没有在另一个答案下评论这件事?编译器不会发出导致部分标志暂停的代码,当然也不会发出C <或<=的代码。但是,当然,如果ZF设置(ecx=0),或者如果CF设置(eax=1的位3),test ecx,ecx/bt eax, 3/jbe将跳转,这会导致大多数CPU上的部分标志暂停,因为它读取的标志并非都来自最后一条写入任何标志的指令。在SandyBridge家族中,它实际上不会停止,只需要插入一个合并的UOP。cmp/test写入所有标志,但bt不修改zf。felixcloudier.com/x86/bt公司