是if( a < 901 )
不是更快if( a <= 900 )
。
与这个简单示例并不完全相同,但是循环复杂代码的性能稍有变化。我想这与生成的机器代码有关,以防万一。
<
比键入快两倍<=
。
是if( a < 901 )
不是更快if( a <= 900 )
。
与这个简单示例并不完全相同,但是循环复杂代码的性能稍有变化。我想这与生成的机器代码有关,以防万一。
<
比键入快两倍<=
。
Answers:
不,在大多数架构上它不会更快。您没有指定,但是在x86上,所有整数比较通常都将在两条机器指令中实现:
test
或cmp
指令,该指令集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:
因此,两者之间的唯一区别是指令jg
与jge
指令。两者将花费相同的时间。
我想指出的是,没有任何内容表明不同的跳转指令花费相同的时间。回答这个问题有些棘手,但是我可以提供以下信息:在《英特尔指令集参考》中,它们都按照一条通用指令分组在一起Jcc
(如果满足条件则跳转)。在“ 优化参考手册 ”的附录C.延迟和吞吐量中将相同的分组在一起。
延迟 -执行内核完成形成指令的所有μop所需的时钟周期数。
吞吐量(T吞吐率) —在发布端口可以自由再次接受同一指令之前需要等待的时钟周期数。对于许多指令,一条指令的吞吐量可以大大小于其延迟
的值为Jcc
:
Latency Throughput
Jcc N/A 0.5
关于以下脚注Jcc
:
7)有条件跳转指令的选择应基于第3.4.1节“分支预测优化”的建议,以提高分支的可预测性。成功预测分支后,的等待时间
jcc
实际上为零。
因此,英特尔文档中对Jcc
任何一条指令的对待都没有别的区别。
如果考虑用于实现指令的实际电路,则可以假定在中的不同位上将存在简单的AND / OR门EFLAGS
,以确定是否满足条件。这样,没有理由测试一个两位的指令所花费的时间比一个测试一个位所花费的时间要长得多或更少(忽略门传播延迟,这比时钟周期要短得多)。
编辑:浮点数
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
jg
和jnle
在同一个指令,7F
:-)
从历史上看(我们所说的是1980年代和1990年代初期),有些架构是正确的。根本问题是整数比较本质上是通过整数减法实现的。这引起以下情况。
Comparison Subtraction
---------- -----------
A < B --> A - B < 0
A = B --> A - B = 0
A > B --> A - B > 0
现在,当A < B
减法必须借用高位以使减法正确时,就像您在手动进行加减时进行借位一样。此“借用”位通常称为进位位,可以通过分支指令进行测试。如果减法等于零(意味着相等),则会设置第二个零位。
通常至少有两个条件转移指令,一个转移到进位,另一个转移到零位。
现在,让我们深入了解问题的核心,让我们扩展上一个表格,使其包含进位和零位结果。
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
可以在一条指令中实现for的分支,因为进位只有在这种情况下才是明确的,也就是说,
;; 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
因此,在某些机器上,使用“小于”比较可能节省一条机器指令。在亚兆赫兹处理器速度和1:1 CPU与内存的速度比时代,这是相关的,但是今天几乎完全不相关了。
jge
,它们同时测试零和符号/进位标志。
<=
测试中可以实现一个指令与交换操作数和测试not <
(相当于>=
)这是所希望的<=
与交换操作数:cmp B,A; bcs addr
。这就是Intel省略该测试的原因,他们认为这是多余的,并且您当时无法负担多余的指令:-)
假设我们正在谈论内部整数类型,则没有一种方法可以比另一种更快。它们在语义上显然是相同的。他们都要求编译器做同样的事情。只有严重损坏的编译器会为其中之一生成劣质代码。
如果有一些平台,<
是速度比<=
为简单的整数类型,编译器应该总是转换<=
到<
为常数。任何不适合该平台的编译器都将是糟糕的编译器。
<
没有<=
速度。当您考虑到编译器通常已经执行了无效代码优化,尾部调用优化,循环提升(有时会展开),各种循环的自动并行化等工作时,这对于编译器而言是非常简单的优化。为什么浪费时间考虑过早的优化?运行原型,分析其轮廓以确定最重要的优化位于何处,按重要性顺序执行这些优化,并在测量进度的过程中再次进行轮廓分析
(a < C)
to 转换(a <= C-1)
(对于某个常数C
)导致C
在指令集中更难编码时。例如,在比较中,一条指令集可能能够以紧凑的形式表示从-127到128的有符号常量,但是该范围之外的常量必须使用更长,更慢的编码或完全使用另一条指令来加载。因此,像这样的比较(a < -127)
可能没有直接的转换。
a > 127
,a > 128
因为您别无选择,只能使用所需的那个。我们比较a > 127
到a >= 128
,不能要求不同的编码或不同的指令,因为它们具有相同的真值表。一个的任何编码等同于另一个的编码。
<=
到<
常量”。据我所知,这种转换涉及更改常数。例如,a <= 42
被编译为a < 43
因为<
速度更快。在某些情况下,这样的转换不会取得成果,因为新常数可能需要更多或更慢的指令。当然a > 127
和a >= 128
是等效的,编译器应该以(相同)最快的方式对这两种形式进行编码,但这与我所说的并不矛盾。
我看到两者都不快。编译器会在每种情况下以不同的值生成相同的机器代码。
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
if(a <=900)
来证明它生成完全相同的asm :)
对于浮点代码,即使在现代体系结构上,<=比较的确可能会更慢(通过一条指令)。这是第一个功能:
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
需要五个指令,而同时compare_strict
需要四个。
您可能会认为编译器可以像这样优化第二个功能:
int compare_loose(double a, double b) { return ! (a > b); }
但是,这将错误地处理NaN。NaN1 <= NaN2
并且都NaN1 > NaN2
需要评估为假。
fucomip
设置ZF和CF。
cr
是相当于像标志ZF
,并CF
在x86。(尽管CR更灵活。)张贴者正在谈论的是将结果移动到GPR:在PowerPC上需要两条指令,但是x86有条件移动指令。
也许那本未命名的书的作者阅读的a > 0
速度快于普通作者,a >= 1
并认为这是正确的。
但这是因为0
涉及到a(因为CMP
可以根据体系结构将其替换为OR
),而不是因为<
。
(a >= 1)
运行慢(a > 0)
,因为前者可以被优化平凡转化为后者..
至少,如果这是真的,则编译器可以将<= b简化为!(a> b),因此,即使比较本身实际上速度较慢,除了最幼稚的编译器之外,您都不会注意到差异。
NOT
是由其他指令(je
相对jne
)制作的
这将高度依赖于C编译到的基础体系结构。某些处理器和体系结构可能具有显式的指令,用于等于或小于和等于的指令,它们以不同的周期数执行。
但是,这将是非常不寻常的,因为编译器可以解决该问题,因此不相关。
对于体系结构,编译器和语言的大多数组合,它不会更快。
其他答案集中在x86架构上,我不知道ARM架构(您的示例汇编程序似乎是这样)足以对生成的代码进行特别注释,但这只是一个微优化的示例,它是非常架构的具体而言,并且有可能是反优化,也有可能是优化。
因此,我建议这种微优化是货物崇拜编程的示例,而不是最佳软件工程实践。
在某些架构中,这可能是一种优化,但我知道至少有一种架构可能是相反的。古老的Transputer体系结构仅具有等于或大于或等于的机器代码指令,因此所有比较都必须从这些原语构建。
即使这样,在几乎所有情况下,编译器仍可以按以下方式对评估指令进行排序:在实践中,没有任何比较比其他任何优势。但是,最坏的情况是,可能需要添加反向指令(REV)来交换操作数堆栈中的前两项。这是一条单字节指令,需要一个周期来运行,因此开销最小。
像这样的微优化是优化还是反优化取决于您使用的特定体系结构,因此养成使用特定于体系结构的微优化的习惯通常是一个坏主意,否则您可能会本能地在不适当的情况下使用一本,而这恰恰是您所阅读的书所提倡的。
即使有任何区别,您也应该不会注意到差异。此外,在实践中,除非您要使用一些魔术常数,否则您将不得不做其他事情a + 1
或a - 1
使条件成立,这从根本上来说是非常糟糕的做法。
您可以说该行在大多数脚本语言中都是正确的,因为多余的字符会导致代码处理稍微慢一些。但是,正如最高答案所指出的那样,它在C ++中应该没有任何作用,并且使用脚本语言完成的任何事情都可能与优化无关。
当我写这个答案时,我只看一般关于<vs. <=的标题问题,而不是常数a < 901
vs 的具体例子a <= 900
。许多编译器总是通过在<
和之间进行转换来缩小常量的大小<=
,例如,因为x86立即操作数的-128..127编码具有较短的1字节编码。
对于ARM尤其是AArch64,能否立即编码取决于能否将狭窄的字段旋转到单词中的任何位置。因此cmp w0, #0x00f000
将是可编码的,而cmp w0, #0x00effff
可能不是。因此,比较和编译时常量比较小的规则并不总是适用于AArch64。
在大多数机器上的汇编语言中,与的比较<=
成本与与的比较成本相同<
。无论您是在其上进行分支,对其进行布尔化以创建一个0/1整数,还是将其用作无分支选择操作的谓词(例如x86 CMOV),这都适用。其他答案仅解决了问题的这一部分。
但是,这个问题与C ++运算符有关,即优化程序的输入。 通常,它们都同样有效。书中的建议听起来完全是伪造的,因为编译器总是可以转换他们在asm中实现的比较。但是至少有一个例外,即使用<=
会意外创建编译器无法优化的内容。
作为循环条件,在某些情况下,<=
它与质上不同<
,这会阻止编译器证明循环不是无限的。 这会带来很大的不同,从而禁用自动矢量化。
与有符号溢出(UB)不同,无符号溢出被定义为以2为基的环绕。使用基于未发生有符号溢出的UB进行优化的编译器进行签名循环计数器通常是安全的:++i <= size
最终总是会变为false。(每个C程序员应该了解的未定义行为)
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 ++源的(定义的和法律上可观察的)行为的方式进行优化。
(一个简单i <= size
的问题也会产生问题,但我认为计算上限是一个更现实的示例,因为它意外地为您不在乎但编译器必须考虑的输入引入了无限循环的可能性。)
在这种情况下,size=0
导致upper_bound=UINT_MAX
,并且i <= UINT_MAX
始终为true。因此,此循环对于是无限的size=0
,即使您作为程序员可能从未打算传递size = 0 ,编译器也必须尊重这一点。如果编译器可以将此函数内联到调用程序中,从而可以证明size = 0是不可能的,那么它很好,它可以像进行优化那样进行优化i < size
。
如果在循环内部不需要的实际值,则asm like if(!size) skip the loop;
do{...}while(--size);
是优化for( i<size )
循环的一种通常有效的方法i
(为什么循环总是被编译为“ do ... while”样式(尾跳)?)。
但这并不能是无限的:如果输入size==0
,我们将得到2 ^ n次迭代。(对for循环 C 中的所有无符号整数进行迭代使得可以对所有包含零的无符号整数表示一个循环,但要像在asm中那样没有进位标志,这并不容易。)
随着循环计数器的回绕成为可能,现代编译器通常只是“放弃”,而没有那么积极地进行优化。
使用无符号会i <= n
打败clang的惯用语识别功能,该功能sum(1 .. n)
基于高斯n * (n+1) / 2
公式以闭合形式优化循环。
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
# 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获得了一个哑循环。
unsigned sum_1_to_n_naive(unsigned n) {
unsigned total = 0;
for (unsigned i = 0 ; i<=n ; ++i)
total += i;
return total;
}
# 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整数加法自动矢量化,i
在XMM寄存器的元素中并行运行4个值。
# "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,因此它没有使用循环计数器的最终值。
<
或<=
。但肯定的是,test ecx,ecx
/ bt eax, 3
/ jbe
将如果ZF设置(ECX == 0),或者如果CF设置(位EAX的3 == 1)跳,导致大部分CPU的部分标志停顿,因为标志它读取并不都来自最后一条写入任何标志的指令。在Sandybridge系列上,它实际上并没有停滞,只需要插入一个合并的uop。 cmp
/ test
写入所有标志,但bt
保留ZF不变。felixcloutier.com/x86/bt
仅当创建计算机的人对布尔逻辑不好时。他们不应该是。
每个比较(>=
<=
>
<
)都可以相同的速度进行。
每个比较的结果只是一个减法(差),然后看它是否为正/负。
(如果msb
设置,则数字为负)
如何检查a >= b
?子a-b >= 0
检查是否a-b
为正。
如何检查a <= b
?子0 <= b-a
检查是否b-a
为正。
如何检查a < b
?子a-b < 0
检查是否a-b
为负。
如何检查a > b
?子0 > b-a
检查是否b-a
为负。
简而言之,计算机可以针对给定的操作在幕后进行此操作:
a >= b
== msb(a-b)==0
a <= b
== msb(b-a)==0
a > b
== msb(b-a)==1
a < b
==msb(a-b)==1
当然还有电脑会实际上并不需要做==0
或==1
两种。
因为==0
它可以msb
将电路中的信号反相。
无论如何,它们肯定不会a >= b
被计算为a>b || a==b
大声笑