if 语句 vs if-else 语句,哪个更快?
If statement vs if-else statement, which is faster?
前几天我和一个朋友就那两个片段争论不休。哪个更快,为什么?
value = 5;
if (condition) {
value = 6;
}
和:
if (condition) {
value = 6;
} else {
value = 5;
}
如果 value
是矩阵呢?
注意:我知道 value = condition ? 6 : 5;
存在,我希望它会更快,但它不是一个选项。
编辑(由于问题暂时搁置,工作人员要求):
- 请考虑主流编译器生成的 x86 程序集 来回答(g++、clang++、vc、mingw)在优化和非优化版本或 MIPS 汇编.
- 当程序集不同时,解释为什么版本更快以及何时(例如"better because no branching and branching has following issue blahblah")
在未优化的代码中,第一个示例总是分配一个变量一次,有时两次。第二个例子只给一个变量赋值一次。条件在两个代码路径上都是相同的,所以这无关紧要。在优化代码中,它取决于编译器。
一如既往,如果您担心,请生成程序集并查看编译器实际在做什么。
TL;DR: 在未优化的代码中,没有 else
的 if
似乎无关紧要地提高了效率,但即使是最基本的优化级别也启用了代码基本上改写为value = condition + 5
.
我 gave it a try 并为以下代码生成程序集:
int ifonly(bool condition, int value)
{
value = 5;
if (condition) {
value = 6;
}
return value;
}
int ifelse(bool condition, int value)
{
if (condition) {
value = 6;
} else {
value = 5;
}
return value;
}
在禁用优化的 gcc 6.3 上 (-O0
),相关差异是:
mov DWORD PTR [rbp-8], 5
cmp BYTE PTR [rbp-4], 0
je .L2
mov DWORD PTR [rbp-8], 6
.L2:
mov eax, DWORD PTR [rbp-8]
对于 ifonly
,而 ifelse
有
cmp BYTE PTR [rbp-4], 0
je .L5
mov DWORD PTR [rbp-8], 6
jmp .L6
.L5:
mov DWORD PTR [rbp-8], 5
.L6:
mov eax, DWORD PTR [rbp-8]
后者看起来效率稍低,因为它有一个额外的跳跃,但两者都有至少两个和最多三个任务,所以除非你真的需要挤压每一滴性能(提示:除非你正在处理 space 穿梭你不会,即使那样你 可能 也不会)差异不会很明显。
然而,即使使用最低的优化级别 (-O1
),两个函数也会减少到相同的程度:
test dil, dil
setne al
movzx eax, al
add eax, 5
基本上等同于
return 5 + condition;
假设 condition
是零或一。
更高的优化级别并没有真正改变输出,除了它们通过在开始时有效地将 EAX
寄存器清零来设法避免 movzx
。
免责声明: 你可能不应该自己写 5 + condition
(即使标准保证将 true
转换为整数类型会得到 1
) 因为对于阅读你代码的人(可能包括你未来的自己)来说,你的意图可能不会立即显而易见。这段代码的目的是表明编译器在这两种情况下产生的结果(实际上)是相同的。 在评论中说得很好:
a human's job is to write code for humans and let the compiler write code for the machine.
的回答表明,对于 int
,它们都针对同一程序集进行了优化,因此无关紧要。
What if value is a matrix ?
我将以更一般的方式解释这一点,即如果 value
是一种构造和赋值成本高(移动成本低)的类型。
然后
T value = init1;
if (condition)
value = init2;
是次优的,因为如果 condition
为真,您将对 init1
进行不必要的初始化,然后进行复制分配。
T value;
if (condition)
value = init2;
else
value = init3;
这样更好。但是如果默认构造是昂贵的并且如果复制构造比初始化更昂贵,那么仍然不是最优的。
你有很好的条件运算符解决方案:
T value = condition ? init1 : init2;
或者,如果您不喜欢条件运算符,您可以像这样创建一个辅助函数:
T create(bool condition)
{
if (condition)
return {init1};
else
return {init2};
}
T value = create(condition);
根据init1
和init2
是什么你也可以这样考虑:
auto final_init = condition ? init1 : init2;
T value = final_init;
但我必须再次强调,只有当给定类型的构造和赋值非常昂贵时,这才有意义。即便如此,也只有通过 分析 你才能确定。
在伪汇编语言中,
li #0, r0
test r1
beq L1
li #1, r0
L1:
可能会也可能不会比
快
test r1
beq L1
li #1, r0
bra L2
L1:
li #0, r0
L2:
取决于实际 CPU 的复杂程度。从最简单到最奇特:
any CPU 大约在 1990 年后制造,良好的性能取决于 instruction cache 内的代码拟合。因此,当有疑问时,尽量减少代码大小。这有利于第一个例子。
有了基本的“in-order, five-stage pipeline" CPU, which is still roughly what you get in many microcontrollers, there is a pipeline bubble 每次分支——有条件的或无条件的——都被采用,所以尽量减少分支指令的数量也很重要。这也有利于第一个例子。
稍微复杂一些 CPUs——花哨的足以做“out-of-order execution", but not fancy enough to use the best known implementations of that concept—may incur pipeline bubbles whenever they encounter write-after-write hazards。这有利于 second 的例子,其中 r0
无论如何只写一次。这些 CPU 通常足够花哨,可以在指令提取器中处理无条件分支,所以你 不是 用写后写惩罚换取分支惩罚。
不知道现在还有没有人在做这种CPU。但是做使用乱序执行的"best known implementations"的CPU很可能会在不常用的指令上偷工减料,所以需要要知道这种事情可能会发生。一个真实的例子是 false data dependencies on the destination registers in popcnt
and lzcnt
on Sandy Bridge CPUs.
在最高端,OOO 引擎最终会为两个代码片段发出完全相同的内部操作序列——这是 "don't worry about it, the compiler will generate the same machine code either way." 的硬件版本但是,代码大小仍然确实很重要,现在您还应该担心条件分支的可预测性。 Branch prediction failures potentially cause a complete pipeline flush, which is catastrophic for performance; see Why is it faster to process a sorted array than an unsorted array? 了解这能带来多大的不同。
如果分支 是 高度不可预测的,并且您的 CPU 有条件设置或条件移动指令,现在是使用它们的时候了:
li #0, r0
test r1
setne r0
或
li #0, r0
li #1, r2
test r1
movne r2, r0
条件集版本也比任何其他版本都更紧凑;如果该指令可用,即使分支是可预测的,它实际上也可以保证是这种情况下的正确指令。条件移动版本需要一个额外的临时寄存器,并且总是浪费一条 li
指令的调度和执行资源;如果分支实际上是可预测的,那么分支版本可能会更快。
是什么让您认为它们中的任何一个甚至是一个班轮更快或更慢?
unsigned int fun0 ( unsigned int condition, unsigned int value )
{
value = 5;
if (condition) {
value = 6;
}
return(value);
}
unsigned int fun1 ( unsigned int condition, unsigned int value )
{
if (condition) {
value = 6;
} else {
value = 5;
}
return(value);
}
unsigned int fun2 ( unsigned int condition, unsigned int value )
{
value = condition ? 6 : 5;
return(value);
}
高级语言的代码行越多,编译器的工作量就越大,因此如果您想对其制定通用规则,请为编译器提供更多的代码。如果算法与上述情况相同,那么人们会期望编译器进行最少的优化来解决这个问题。
00000000 <fun0>:
0: e3500000 cmp r0, #0
4: 03a00005 moveq r0, #5
8: 13a00006 movne r0, #6
c: e12fff1e bx lr
00000010 <fun1>:
10: e3500000 cmp r0, #0
14: 13a00006 movne r0, #6
18: 03a00005 moveq r0, #5
1c: e12fff1e bx lr
00000020 <fun2>:
20: e3500000 cmp r0, #0
24: 13a00006 movne r0, #6
28: 03a00005 moveq r0, #5
2c: e12fff1e bx lr
虽然第一个函数的执行顺序不同,但执行时间相同,这不足为奇。
0000000000000000 <fun0>:
0: 7100001f cmp w0, #0x0
4: 1a9f07e0 cset w0, ne
8: 11001400 add w0, w0, #0x5
c: d65f03c0 ret
0000000000000010 <fun1>:
10: 7100001f cmp w0, #0x0
14: 1a9f07e0 cset w0, ne
18: 11001400 add w0, w0, #0x5
1c: d65f03c0 ret
0000000000000020 <fun2>:
20: 7100001f cmp w0, #0x0
24: 1a9f07e0 cset w0, ne
28: 11001400 add w0, w0, #0x5
2c: d65f03c0 ret
希望你明白如果不同的实现实际上并没有不同不是很明显的话你可以尝试这个。
就矩阵而言,不确定它有多重要,
if(condition)
{
big blob of code a
}
else
{
big blob of code b
}
只是将相同的 if-then-else 包装器放在大块代码周围,无论它们的值 = 5 还是更复杂的东西。同样,比较即使它是一大块代码,它仍然必须被计算,并且等于或不等于某物通常被编译为否定,如果(条件)做某事通常被编译为好像不是条件goto。
00000000 <fun0>:
0: 0f 93 tst r15
2: 03 24 jz $+8 ;abs 0xa
4: 3f 40 06 00 mov #6, r15 ;#0x0006
8: 30 41 ret
a: 3f 40 05 00 mov #5, r15 ;#0x0005
e: 30 41 ret
00000010 <fun1>:
10: 0f 93 tst r15
12: 03 20 jnz $+8 ;abs 0x1a
14: 3f 40 05 00 mov #5, r15 ;#0x0005
18: 30 41 ret
1a: 3f 40 06 00 mov #6, r15 ;#0x0006
1e: 30 41 ret
00000020 <fun2>:
20: 0f 93 tst r15
22: 03 20 jnz $+8 ;abs 0x2a
24: 3f 40 05 00 mov #5, r15 ;#0x0005
28: 30 41 ret
2a: 3f 40 06 00 mov #6, r15 ;#0x0006
2e: 30 41
我们最近刚刚在 Whosebug 上与其他人完成了这个练习。有趣的是,在那种情况下,这个 mips 编译器不仅意识到函数是相同的,而且让一个函数简单地跳转到另一个函数以节省代码 space。不过这里没有这样做
00000000 <fun0>:
0: 0004102b sltu ,[=15=],
4: 03e00008 jr
8: 24420005 addiu ,,5
0000000c <fun1>:
c: 0004102b sltu ,[=15=],
10: 03e00008 jr
14: 24420005 addiu ,,5
00000018 <fun2>:
18: 0004102b sltu ,[=15=],
1c: 03e00008 jr
20: 24420005 addiu ,,5
更多目标。
00000000 <_fun0>:
0: 1166 mov r5, -(sp)
2: 1185 mov sp, r5
4: 0bf5 0004 tst 4(r5)
8: 0304 beq 12 <_fun0+0x12>
a: 15c0 0006 mov , r0
e: 1585 mov (sp)+, r5
10: 0087 rts pc
12: 15c0 0005 mov , r0
16: 1585 mov (sp)+, r5
18: 0087 rts pc
0000001a <_fun1>:
1a: 1166 mov r5, -(sp)
1c: 1185 mov sp, r5
1e: 0bf5 0004 tst 4(r5)
22: 0204 bne 2c <_fun1+0x12>
24: 15c0 0005 mov , r0
28: 1585 mov (sp)+, r5
2a: 0087 rts pc
2c: 15c0 0006 mov , r0
30: 1585 mov (sp)+, r5
32: 0087 rts pc
00000034 <_fun2>:
34: 1166 mov r5, -(sp)
36: 1185 mov sp, r5
38: 0bf5 0004 tst 4(r5)
3c: 0204 bne 46 <_fun2+0x12>
3e: 15c0 0005 mov , r0
42: 1585 mov (sp)+, r5
44: 0087 rts pc
46: 15c0 0006 mov , r0
4a: 1585 mov (sp)+, r5
4c: 0087 rts pc
00000000 <fun0>:
0: 00a03533 snez x10,x10
4: 0515 addi x10,x10,5
6: 8082 ret
00000008 <fun1>:
8: 00a03533 snez x10,x10
c: 0515 addi x10,x10,5
e: 8082 ret
00000010 <fun2>:
10: 00a03533 snez x10,x10
14: 0515 addi x10,x10,5
16: 8082 ret
和编译器
有了这个我的代码,人们希望不同的目标也能匹配
define i32 @fun0(i32 %condition, i32 %value) #0 {
%1 = icmp ne i32 %condition, 0
%. = select i1 %1, i32 6, i32 5
ret i32 %.
}
; Function Attrs: norecurse nounwind readnone
define i32 @fun1(i32 %condition, i32 %value) #0 {
%1 = icmp eq i32 %condition, 0
%. = select i1 %1, i32 5, i32 6
ret i32 %.
}
; Function Attrs: norecurse nounwind readnone
define i32 @fun2(i32 %condition, i32 %value) #0 {
%1 = icmp ne i32 %condition, 0
%2 = select i1 %1, i32 6, i32 5
ret i32 %2
}
00000000 <fun0>:
0: e3a01005 mov r1, #5
4: e3500000 cmp r0, #0
8: 13a01006 movne r1, #6
c: e1a00001 mov r0, r1
10: e12fff1e bx lr
00000014 <fun1>:
14: e3a01006 mov r1, #6
18: e3500000 cmp r0, #0
1c: 03a01005 moveq r1, #5
20: e1a00001 mov r0, r1
24: e12fff1e bx lr
00000028 <fun2>:
28: e3a01005 mov r1, #5
2c: e3500000 cmp r0, #0
30: 13a01006 movne r1, #6
34: e1a00001 mov r0, r1
38: e12fff1e bx lr
fun0:
push.w r4
mov.w r1, r4
mov.w r15, r12
mov.w #6, r15
cmp.w #0, r12
jne .LBB0_2
mov.w #5, r15
.LBB0_2:
pop.w r4
ret
fun1:
push.w r4
mov.w r1, r4
mov.w r15, r12
mov.w #5, r15
cmp.w #0, r12
jeq .LBB1_2
mov.w #6, r15
.LBB1_2:
pop.w r4
ret
fun2:
push.w r4
mov.w r1, r4
mov.w r15, r12
mov.w #6, r15
cmp.w #0, r12
jne .LBB2_2
mov.w #5, r15
.LBB2_2:
pop.w r4
ret
现在从技术上讲,这些解决方案中的一些解决方案存在性能差异,有时结果是 5 个 case 跳过结果是 6 个代码,反之亦然,分支比执行更快吗?有人可能会争论,但执行应该有所不同。但这更像是代码中的 if 条件与 if not 条件,导致编译器执行 if this jump over else execute through。但这不一定是由于编码风格,而是比较以及 if 和 else 情况下的任何语法。
好的,既然汇编是标签之一,我就假设你的代码是伪代码(不一定是 c),并由人工将其翻译成 6502 汇编。
第一个选项(没有其他)
ldy #[=10=]
lda #
dey
bmi false
lda #
false brk
第二个选项(还有其他)
ldy #[=11=]
dey
bmi else
lda #
sec
bcs end
else lda #
end brk
假设:条件在 Y 寄存器中,在任一选项的第一行将其设置为 0 或 1,结果将在累加器中。
所以,在计算了每种情况的两种可能性的循环之后,我们发现第一个构造通常更快;条件为 0 时为 9 个周期,条件为 1 时为 10 个周期,而选项二在条件为 0 时也是 9 个周期,但条件为 1 时为 13 个周期。(周期计数不包括 BRK
最后).
结论:If only
比 If-Else
构造更快。
为了完整起见,这里有一个优化的 value = condition + 5
解决方案:
ldy #[=12=]
lda #[=12=]
tya
adc #
brk
这将我们的时间减少到 8 个周期(同样不包括最后的 BRK
)。
前几天我和一个朋友就那两个片段争论不休。哪个更快,为什么?
value = 5;
if (condition) {
value = 6;
}
和:
if (condition) {
value = 6;
} else {
value = 5;
}
如果 value
是矩阵呢?
注意:我知道 value = condition ? 6 : 5;
存在,我希望它会更快,但它不是一个选项。
编辑(由于问题暂时搁置,工作人员要求):
- 请考虑主流编译器生成的 x86 程序集 来回答(g++、clang++、vc、mingw)在优化和非优化版本或 MIPS 汇编.
- 当程序集不同时,解释为什么版本更快以及何时(例如"better because no branching and branching has following issue blahblah")
在未优化的代码中,第一个示例总是分配一个变量一次,有时两次。第二个例子只给一个变量赋值一次。条件在两个代码路径上都是相同的,所以这无关紧要。在优化代码中,它取决于编译器。
一如既往,如果您担心,请生成程序集并查看编译器实际在做什么。
TL;DR: 在未优化的代码中,没有 else
的 if
似乎无关紧要地提高了效率,但即使是最基本的优化级别也启用了代码基本上改写为value = condition + 5
.
我 gave it a try 并为以下代码生成程序集:
int ifonly(bool condition, int value)
{
value = 5;
if (condition) {
value = 6;
}
return value;
}
int ifelse(bool condition, int value)
{
if (condition) {
value = 6;
} else {
value = 5;
}
return value;
}
在禁用优化的 gcc 6.3 上 (-O0
),相关差异是:
mov DWORD PTR [rbp-8], 5
cmp BYTE PTR [rbp-4], 0
je .L2
mov DWORD PTR [rbp-8], 6
.L2:
mov eax, DWORD PTR [rbp-8]
对于 ifonly
,而 ifelse
有
cmp BYTE PTR [rbp-4], 0
je .L5
mov DWORD PTR [rbp-8], 6
jmp .L6
.L5:
mov DWORD PTR [rbp-8], 5
.L6:
mov eax, DWORD PTR [rbp-8]
后者看起来效率稍低,因为它有一个额外的跳跃,但两者都有至少两个和最多三个任务,所以除非你真的需要挤压每一滴性能(提示:除非你正在处理 space 穿梭你不会,即使那样你 可能 也不会)差异不会很明显。
然而,即使使用最低的优化级别 (-O1
),两个函数也会减少到相同的程度:
test dil, dil
setne al
movzx eax, al
add eax, 5
基本上等同于
return 5 + condition;
假设 condition
是零或一。
更高的优化级别并没有真正改变输出,除了它们通过在开始时有效地将 EAX
寄存器清零来设法避免 movzx
。
免责声明: 你可能不应该自己写 5 + condition
(即使标准保证将 true
转换为整数类型会得到 1
) 因为对于阅读你代码的人(可能包括你未来的自己)来说,你的意图可能不会立即显而易见。这段代码的目的是表明编译器在这两种情况下产生的结果(实际上)是相同的。
a human's job is to write code for humans and let the compiler write code for the machine.
int
,它们都针对同一程序集进行了优化,因此无关紧要。
What if value is a matrix ?
我将以更一般的方式解释这一点,即如果 value
是一种构造和赋值成本高(移动成本低)的类型。
然后
T value = init1;
if (condition)
value = init2;
是次优的,因为如果 condition
为真,您将对 init1
进行不必要的初始化,然后进行复制分配。
T value;
if (condition)
value = init2;
else
value = init3;
这样更好。但是如果默认构造是昂贵的并且如果复制构造比初始化更昂贵,那么仍然不是最优的。
你有很好的条件运算符解决方案:
T value = condition ? init1 : init2;
或者,如果您不喜欢条件运算符,您可以像这样创建一个辅助函数:
T create(bool condition)
{
if (condition)
return {init1};
else
return {init2};
}
T value = create(condition);
根据init1
和init2
是什么你也可以这样考虑:
auto final_init = condition ? init1 : init2;
T value = final_init;
但我必须再次强调,只有当给定类型的构造和赋值非常昂贵时,这才有意义。即便如此,也只有通过 分析 你才能确定。
在伪汇编语言中,
li #0, r0
test r1
beq L1
li #1, r0
L1:
可能会也可能不会比
快 test r1
beq L1
li #1, r0
bra L2
L1:
li #0, r0
L2:
取决于实际 CPU 的复杂程度。从最简单到最奇特:
any CPU 大约在 1990 年后制造,良好的性能取决于 instruction cache 内的代码拟合。因此,当有疑问时,尽量减少代码大小。这有利于第一个例子。
有了基本的“in-order, five-stage pipeline" CPU, which is still roughly what you get in many microcontrollers, there is a pipeline bubble 每次分支——有条件的或无条件的——都被采用,所以尽量减少分支指令的数量也很重要。这也有利于第一个例子。
稍微复杂一些 CPUs——花哨的足以做“out-of-order execution", but not fancy enough to use the best known implementations of that concept—may incur pipeline bubbles whenever they encounter write-after-write hazards。这有利于 second 的例子,其中
r0
无论如何只写一次。这些 CPU 通常足够花哨,可以在指令提取器中处理无条件分支,所以你 不是 用写后写惩罚换取分支惩罚。不知道现在还有没有人在做这种CPU。但是做使用乱序执行的"best known implementations"的CPU很可能会在不常用的指令上偷工减料,所以需要要知道这种事情可能会发生。一个真实的例子是 false data dependencies on the destination registers in
popcnt
andlzcnt
on Sandy Bridge CPUs.在最高端,OOO 引擎最终会为两个代码片段发出完全相同的内部操作序列——这是 "don't worry about it, the compiler will generate the same machine code either way." 的硬件版本但是,代码大小仍然确实很重要,现在您还应该担心条件分支的可预测性。 Branch prediction failures potentially cause a complete pipeline flush, which is catastrophic for performance; see Why is it faster to process a sorted array than an unsorted array? 了解这能带来多大的不同。
如果分支 是 高度不可预测的,并且您的 CPU 有条件设置或条件移动指令,现在是使用它们的时候了:
li #0, r0 test r1 setne r0
或
li #0, r0 li #1, r2 test r1 movne r2, r0
条件集版本也比任何其他版本都更紧凑;如果该指令可用,即使分支是可预测的,它实际上也可以保证是这种情况下的正确指令。条件移动版本需要一个额外的临时寄存器,并且总是浪费一条
li
指令的调度和执行资源;如果分支实际上是可预测的,那么分支版本可能会更快。
是什么让您认为它们中的任何一个甚至是一个班轮更快或更慢?
unsigned int fun0 ( unsigned int condition, unsigned int value )
{
value = 5;
if (condition) {
value = 6;
}
return(value);
}
unsigned int fun1 ( unsigned int condition, unsigned int value )
{
if (condition) {
value = 6;
} else {
value = 5;
}
return(value);
}
unsigned int fun2 ( unsigned int condition, unsigned int value )
{
value = condition ? 6 : 5;
return(value);
}
高级语言的代码行越多,编译器的工作量就越大,因此如果您想对其制定通用规则,请为编译器提供更多的代码。如果算法与上述情况相同,那么人们会期望编译器进行最少的优化来解决这个问题。
00000000 <fun0>:
0: e3500000 cmp r0, #0
4: 03a00005 moveq r0, #5
8: 13a00006 movne r0, #6
c: e12fff1e bx lr
00000010 <fun1>:
10: e3500000 cmp r0, #0
14: 13a00006 movne r0, #6
18: 03a00005 moveq r0, #5
1c: e12fff1e bx lr
00000020 <fun2>:
20: e3500000 cmp r0, #0
24: 13a00006 movne r0, #6
28: 03a00005 moveq r0, #5
2c: e12fff1e bx lr
虽然第一个函数的执行顺序不同,但执行时间相同,这不足为奇。
0000000000000000 <fun0>:
0: 7100001f cmp w0, #0x0
4: 1a9f07e0 cset w0, ne
8: 11001400 add w0, w0, #0x5
c: d65f03c0 ret
0000000000000010 <fun1>:
10: 7100001f cmp w0, #0x0
14: 1a9f07e0 cset w0, ne
18: 11001400 add w0, w0, #0x5
1c: d65f03c0 ret
0000000000000020 <fun2>:
20: 7100001f cmp w0, #0x0
24: 1a9f07e0 cset w0, ne
28: 11001400 add w0, w0, #0x5
2c: d65f03c0 ret
希望你明白如果不同的实现实际上并没有不同不是很明显的话你可以尝试这个。
就矩阵而言,不确定它有多重要,
if(condition)
{
big blob of code a
}
else
{
big blob of code b
}
只是将相同的 if-then-else 包装器放在大块代码周围,无论它们的值 = 5 还是更复杂的东西。同样,比较即使它是一大块代码,它仍然必须被计算,并且等于或不等于某物通常被编译为否定,如果(条件)做某事通常被编译为好像不是条件goto。
00000000 <fun0>:
0: 0f 93 tst r15
2: 03 24 jz $+8 ;abs 0xa
4: 3f 40 06 00 mov #6, r15 ;#0x0006
8: 30 41 ret
a: 3f 40 05 00 mov #5, r15 ;#0x0005
e: 30 41 ret
00000010 <fun1>:
10: 0f 93 tst r15
12: 03 20 jnz $+8 ;abs 0x1a
14: 3f 40 05 00 mov #5, r15 ;#0x0005
18: 30 41 ret
1a: 3f 40 06 00 mov #6, r15 ;#0x0006
1e: 30 41 ret
00000020 <fun2>:
20: 0f 93 tst r15
22: 03 20 jnz $+8 ;abs 0x2a
24: 3f 40 05 00 mov #5, r15 ;#0x0005
28: 30 41 ret
2a: 3f 40 06 00 mov #6, r15 ;#0x0006
2e: 30 41
我们最近刚刚在 Whosebug 上与其他人完成了这个练习。有趣的是,在那种情况下,这个 mips 编译器不仅意识到函数是相同的,而且让一个函数简单地跳转到另一个函数以节省代码 space。不过这里没有这样做
00000000 <fun0>:
0: 0004102b sltu ,[=15=],
4: 03e00008 jr
8: 24420005 addiu ,,5
0000000c <fun1>:
c: 0004102b sltu ,[=15=],
10: 03e00008 jr
14: 24420005 addiu ,,5
00000018 <fun2>:
18: 0004102b sltu ,[=15=],
1c: 03e00008 jr
20: 24420005 addiu ,,5
更多目标。
00000000 <_fun0>:
0: 1166 mov r5, -(sp)
2: 1185 mov sp, r5
4: 0bf5 0004 tst 4(r5)
8: 0304 beq 12 <_fun0+0x12>
a: 15c0 0006 mov , r0
e: 1585 mov (sp)+, r5
10: 0087 rts pc
12: 15c0 0005 mov , r0
16: 1585 mov (sp)+, r5
18: 0087 rts pc
0000001a <_fun1>:
1a: 1166 mov r5, -(sp)
1c: 1185 mov sp, r5
1e: 0bf5 0004 tst 4(r5)
22: 0204 bne 2c <_fun1+0x12>
24: 15c0 0005 mov , r0
28: 1585 mov (sp)+, r5
2a: 0087 rts pc
2c: 15c0 0006 mov , r0
30: 1585 mov (sp)+, r5
32: 0087 rts pc
00000034 <_fun2>:
34: 1166 mov r5, -(sp)
36: 1185 mov sp, r5
38: 0bf5 0004 tst 4(r5)
3c: 0204 bne 46 <_fun2+0x12>
3e: 15c0 0005 mov , r0
42: 1585 mov (sp)+, r5
44: 0087 rts pc
46: 15c0 0006 mov , r0
4a: 1585 mov (sp)+, r5
4c: 0087 rts pc
00000000 <fun0>:
0: 00a03533 snez x10,x10
4: 0515 addi x10,x10,5
6: 8082 ret
00000008 <fun1>:
8: 00a03533 snez x10,x10
c: 0515 addi x10,x10,5
e: 8082 ret
00000010 <fun2>:
10: 00a03533 snez x10,x10
14: 0515 addi x10,x10,5
16: 8082 ret
和编译器
有了这个我的代码,人们希望不同的目标也能匹配
define i32 @fun0(i32 %condition, i32 %value) #0 {
%1 = icmp ne i32 %condition, 0
%. = select i1 %1, i32 6, i32 5
ret i32 %.
}
; Function Attrs: norecurse nounwind readnone
define i32 @fun1(i32 %condition, i32 %value) #0 {
%1 = icmp eq i32 %condition, 0
%. = select i1 %1, i32 5, i32 6
ret i32 %.
}
; Function Attrs: norecurse nounwind readnone
define i32 @fun2(i32 %condition, i32 %value) #0 {
%1 = icmp ne i32 %condition, 0
%2 = select i1 %1, i32 6, i32 5
ret i32 %2
}
00000000 <fun0>:
0: e3a01005 mov r1, #5
4: e3500000 cmp r0, #0
8: 13a01006 movne r1, #6
c: e1a00001 mov r0, r1
10: e12fff1e bx lr
00000014 <fun1>:
14: e3a01006 mov r1, #6
18: e3500000 cmp r0, #0
1c: 03a01005 moveq r1, #5
20: e1a00001 mov r0, r1
24: e12fff1e bx lr
00000028 <fun2>:
28: e3a01005 mov r1, #5
2c: e3500000 cmp r0, #0
30: 13a01006 movne r1, #6
34: e1a00001 mov r0, r1
38: e12fff1e bx lr
fun0:
push.w r4
mov.w r1, r4
mov.w r15, r12
mov.w #6, r15
cmp.w #0, r12
jne .LBB0_2
mov.w #5, r15
.LBB0_2:
pop.w r4
ret
fun1:
push.w r4
mov.w r1, r4
mov.w r15, r12
mov.w #5, r15
cmp.w #0, r12
jeq .LBB1_2
mov.w #6, r15
.LBB1_2:
pop.w r4
ret
fun2:
push.w r4
mov.w r1, r4
mov.w r15, r12
mov.w #6, r15
cmp.w #0, r12
jne .LBB2_2
mov.w #5, r15
.LBB2_2:
pop.w r4
ret
现在从技术上讲,这些解决方案中的一些解决方案存在性能差异,有时结果是 5 个 case 跳过结果是 6 个代码,反之亦然,分支比执行更快吗?有人可能会争论,但执行应该有所不同。但这更像是代码中的 if 条件与 if not 条件,导致编译器执行 if this jump over else execute through。但这不一定是由于编码风格,而是比较以及 if 和 else 情况下的任何语法。
好的,既然汇编是标签之一,我就假设你的代码是伪代码(不一定是 c),并由人工将其翻译成 6502 汇编。
第一个选项(没有其他)
ldy #[=10=]
lda #
dey
bmi false
lda #
false brk
第二个选项(还有其他)
ldy #[=11=]
dey
bmi else
lda #
sec
bcs end
else lda #
end brk
假设:条件在 Y 寄存器中,在任一选项的第一行将其设置为 0 或 1,结果将在累加器中。
所以,在计算了每种情况的两种可能性的循环之后,我们发现第一个构造通常更快;条件为 0 时为 9 个周期,条件为 1 时为 10 个周期,而选项二在条件为 0 时也是 9 个周期,但条件为 1 时为 13 个周期。(周期计数不包括 BRK
最后).
结论:If only
比 If-Else
构造更快。
为了完整起见,这里有一个优化的 value = condition + 5
解决方案:
ldy #[=12=]
lda #[=12=]
tya
adc #
brk
这将我们的时间减少到 8 个周期(同样不包括最后的 BRK
)。