三目运算符和if_else引发的血案

时间:2021-06-18 17:09:02

三目运算符和if_else引发的血案

背景

刚刚入职,在看各种代码。在很多业务逻辑的判读通篇都是用的if-else,思考:程序猿都是爱偷懒的,对于一些简单的逻辑能否该用三目运算符呢,这样整个代码也不会显得十分冗余,在简洁度上面看起来也比较舒服。
既然三目运算符相比与if-else来说,比较简洁,那么他们在性能上又有没差异呢?
结论是:三目运算符的运算速度比if-else的效率高出1~0.5倍左右,当然机器可能也会导致误差和结果波动,百度上有测出2倍多的 ,对着数据表示怀疑- -!!
下文基于三目运算符多效率为什比if-else效率高做了分析

码上告诉你

StopWatch sw = new StopWatch();
sw.start("if-else");
for (int i = 1; i <= size; i++) {
if (a > b) {
temp = a;
} else {
temp = b;
}
}
sw.stop();

sw.start("Tri-");
for (int i = 1; i <= size; i++) {
temp = a > b ? a : b;
}
sw.stop();

System.out.println(sw.prettyPrint());

结果输出

size = 10000;
StopWatch '': running time (millis) = 3
-----------------------------------------

ms % Task name
-----------------------------------------

00002 067% if-else
00001 033% Tri-

size = 10 0000
StopWatch '': running time (millis) = 29
-----------------------------------------

ms % Task name
-----------------------------------------

00016 055% if-else
00013 045% Tri-

size = 100 0000
StopWatch '': running time (millis) = 279
-----------------------------------------

ms % Task name
-----------------------------------------

00150 054% if-else
00129 046% Tri-

ps:这里为了直观显示结果,选用stopwatch来计时;时间统计可以用stopwatch或者System.currentTimeMillis,stopWatch对小数做了过滤,如果自己想测试的话,建议用currentTimeMills这个可以更加直观的看到数值末尾的波动

改用:System.currentTimeMillis 的计算得出的结果
max = If-else_time/ Tri_time
size = 100 0000
[ 9 ] max :1.8148148148148149-------- min : 1.0714285714285714
-----max-----: 1.8148148148148149
-----avg-----: 1.292012617012617
-----min-----: 1.0714285714285714

现象看本质

问题剖析

解决问题的思路就是根据这个:java–>class字节码–>汇编

三目运算符和if_else引发的血案

字节码层面上剖析问题

首先上查看if-else 和 Tri(三目运算符)生成的字节码文件
linweibodeMacBook-Pro:compare linweibo$ javap -c TriAndIf

[a = 100; b = 500] -----肯定会走 else
void funTri();
Code:
0: bipush 100
2: istore_2
3: sipush 500
6: istore_3
7: iload_2
8: iload_3
9: if_icmple 16
12: iload_2
13: goto 17
16: iload_3
17: istore_1
18: return


void funIF();
Code:
0: bipush 100
2: istore_2
3: sipush 500
6: istore_3
7: iload_2
8: iload_3
9: if_icmple 17
12: iload_2
13: istore_1 !!!!!
14: goto 19
17: iload_3
18: istore_1
19: return

============分隔符===============================
[a = 100 ; b = 50]----短路判断走不走,else
void funTri();
Code:
0: bipush 100
2: istore_2
3: bipush 50
5: istore_3
6: iload_2
7: iload_3
8: if_icmple 15
11: iload_2
12: goto 16
15: iload_3
16: istore_1
17: return

void funIF();
Code:
0: bipush 100
2: istore_2
3: bipush 50
5: istore_3
6: iload_2
7: iload_3
8: if_icmple 16
11: iload_2
12: istore_1 !!!!!
13: goto 18
16: iload_3
17: istore_1
18: return

截取一段字节码做分析

void funTri();
Code:
0: bipush 100.
//当int取值-128~127时,JVM采用bipush指令将常量压入栈中
2: istore_2
3: sipush 500
//当int取值-32768~32767时,JVM采用sipush指令将常量压入栈中
6: istore_3
//把引用保存到局部变量表中的索引3的位置,然后引用弹出栈
7: iload_2
//把局部变量表中的索引1处的值压入操作栈
8: iload_3
9: if_icmple 16
//比较 iload_2 和 iload_3的值,如果 iload_2 <= iload_3 则跳转到第 16 行代码
12: iload_2
13: goto 17
16: iload_3
17: istore_1
18: return

iload_2 和 iload_3 两个指令将两个入参压入操作数栈中;if_icmple会比较栈顶的两个值的大小;如果 a 小都等于 b 值的话,会跳转到 第 16 条字节码处执行。

三目运算符和if_else引发的血案
这里需要引进一个概念:java虚拟机字节码执行引擎

jvm字节码执行引擎时jvm最核心的组成部分之一。它做的事情很简单:输入的是字节码文件,处理过程是字节码解析等效过程,输出的是执行结果。
在这里java的跨平台性就得到了很好的解释:java代码通过编译器编译之后便会生成一个字节码文件,字节码事一种二进制的class文件,它的内容是jvm的指令,而不像c/c++经由编译器直接生成机器码(汇编)。在java中不用担心生成的字节码文件的兼容性,因为所有的jvm都遵守java虚拟机规范,也就是说所有的jvm环境都是一样的,这样一来字节码文件可以在不同平台下的jvm上运行,这也就是我们常说的java的跨平台性。

再啰嗦一下,有得必有失,虽然保证的跨平台性,代价就是java比c/c++性能低的原因之一就是:语言翻译过程中多了一个解析成字节码的环节

三目运算符和if_else引发的血案


  • 回到主题中,三目运算符效率比if_else速度快:在jvm解析class文件这一环节中三目运算符的字节码比if_else少了一个操作指令,这是因为else里头也可以存储

三目运算符和if_else引发的血案

在if_else或者三目运算符外部嵌套高密度for;或者在更加真实的业务场景中可能不需要外加for,里头可能是调用方法或者对象,还有可能递归,那么很有可能持有对象引用太深导致stack区内存泄漏

汇编层面上剖析问题

  • 在VC++6.0中c/C++的if_else 和 三目运算符的 汇编代码
37: if(a>b)
00401079 mov ecx,dword ptr [ebp-10h]
0040107C cmp ecx,dword ptr [ebp-14h]
0040107F jle main+79h (00401089)
38: temp=a;

00401081 mov edx,dword ptr [ebp-10h]
00401084 mov dword ptr [ebp-18h],edx
39: else
00401087 jmp main+7Fh (0040108f)
40: temp=b;

00401089 mov eax,dword ptr [ebp-14h]
0040108C mov dword ptr [ebp-18h],eax


51: temp=a>b?a:b;
004010F3 mov edx,dword ptr [ebp-10h]
004010F6 cmp edx,dword ptr [ebp-14h]
004010F9 jle main+0F3h (00401103)
004010FB mov eax,dword ptr [ebp-10h]
004010FE mov dword ptr [ebp-24h],eax
00401101 jmp main+0F9h (00401109)
00401103 mov ecx,dword ptr [ebp-14h]
00401106 mov dword ptr [ebp-24h],ecx
00401109 mov edx,dword ptr [ebp-24h]
0040110C mov dword ptr [ebp-18h],edx

If-else无论在何种情况下(在if中或者else中),都是通过先将需要赋的变量值传给寄存器然后再通过寄存器赋值给temp变量 。即
mov edx,b; mov temp,edx ;
然而,对于三目运算,它其中一步却增加多了一个临时变量。
mov ecx,b;mov NEWTEMP,ecx;
mov edx,NEWTEMP;mov temp;edx
因为三目运算是先运算,再赋值!
例如 :
temp = a > b ? a : b ;
a > b ? a : b 是运算, temp = (a > b ? a : b )是赋值。

if语句是直接赋值,不存在运算,所以快一点;
这也就可以解释为什么有时候,if_else 会比 三目执行的快
但是呢,编译器已经帮我们做了优化,所以在汇编成面上,三木运算符和if_else 在效率低差异化上是比较小的。
针对java语言,java看的是字节码不是汇编

总结

  • 三木运算符的效率比if_else效率高的原因

1.在java转成字节码指令时,if_else比三目运算符多了一条指令istore指令
2.因为解析字节码是在jvm的字节码执行引擎中所以性能主要是在这里被损耗

  • 建议
  1. 对于简单的业务逻辑尽量替代为三目运算符,就算不为性能,起码看起来也比较爽代码
  2. 对于一些核心的业务逻辑,可以进行抽取,利用状态设计模式

ps:时间比较仓猝考虑的不太全面和漏洞,感兴趣的一起剥坑,测试代码中还遇到一个问题,就是在运行多组测试数据时后面的数据会出现Infiity ,只有单步调试才不会出现。