请用5秒钟的时间查看下面的代码是否存在bug。
OK,熟练的程序猿应该已经发现Bug所在了,在第8行和第10行下面我没有添加关键字break; 这就导致这段代码的行为逻辑与我的设计初衷不符了。
缺点一、语法正确,逻辑错误
这就是第一个理由为什么程序猿很少使用switch来做条件判断,对于新手来说忘记写break实在是再普通不过了,就算是老猿忘记写也是时有发生的事情,而这个语法错误在诸多的语法检查器上没有办法检查出来的,因为从语法角度来说是正确的!可是代码的处理逻辑却是错误的!用if来重写这段代码的话,就不会发生这种错误。
上面的代码为了保证正确我添加了else做一个逻辑上的保证,其实如果不写else,这段代码也不会发生逻辑错误,而且一旦我忘记写花括号的时候,语法编译器是会提示我添加的,甚至可以使用eslint这种的工具强制我使用花括号,这样就不会犯语法错误了,一旦出现bug,那么肯定是我逻辑上的问题了。
缺点二、死板的语法
switch尽管对于break很宽容,但是对判断条件很严苛,case后面只能跟常量,如果你用C编写的话,甚至只能用int类型作为判断条件。对于我们这么潇洒自如的程序员来说,这种限制实在是太麻烦了,用if的话,别说是常量了,我用函数都可以,真正做到方便快捷。
缺点三、需要子函数来处理分支
这个缺点跟缺点一有关,为了防止漏写break,因此建议把分支处理方法独立成一个子函数来处理,这样在阅读代码的时候就会减少忘记写break带来的bug,那么用if来写的话,我想怎么写就怎么写,非常随意自由,但是这也导致了代码的可读性大大降低。
switch的优点
既然switch有这么严重的缺点,那怎么在所有语言中依然会存在呢?那就说下switch的优点吧,它的优点也刚好是它的缺点。
在很久很久以前,那时候的电脑性能还不如一台小霸学习机的时候,聪明的计算机科学家为了提高计算机的处理速度,将一些逻辑分支处理方法简化了一下,关注公众号小黄鸭编程社区,回复关键字手册,获取最新开发手册。把一些需要做逻辑判断的操作给固定死,然后只要查表一样一个一个对一下就能做出相应的反应了。
比如说a=0的判断,switch和if在cpu上面的处理方式是不一样的,switch是在编译阶段将子函数的地址和判断条件绑定了,只要直接将a的直接映射到子函数地址去执行就可以了,但是if处理起来就不一样了。
它首先要把a的值放到CPU的寄存器中,然后要把比较的值放到CPU的另一个寄存器中,然后做减法,然后根据计算结果跳转到子函数去执行,这样一来就要多出3步的操作了,如果逻辑判断多的话,那么将会比switch多处许多倍的操作,尽管寄存器操作的速度很快,但是对于当时的学习机来说,这点速度根本不够用啊。
那还有一个问题,为什么要使用break来做一个判断结束呢?这不是很容易造成语法错误了?那就要说到子函数的问题上了。
在早期的电脑代码中是没有子函数的概念的,那时候都是用goto随意跳转的,你想去第10行代码,很简单goto 10就可以了。这种编程思维在C的早期阶段还是一直受到影响的,因此早期的C也没有子函数,都是一堆逻辑处理混乱在一起,goto满天飞,所以那时候你没有一个最强大脑是写不了程序的。那为了告诉程序我这里条件判断处理结束,就添加了break作为终止符号。后来慢慢的有了子程序,有了更好的编程规范,才一步一步的将写代码沦落到体力劳动。
后来发展的新语言为了标榜自己的血统,多少都要参考下C,然后就把switch这种诡异的语法也继承下来了。但是也不是所有的语言都照搬,比如Google发明的新语言golang和kotlin就又把switch包装了一下,去掉了令人误会的语法,又让switch变得灵活起来了,对了,在代码重构的时候,还是用switch吧,这样看起来的确代码更简洁哦!
说来也是巧最近在看 Dubbo 源码,然后发现了一处很奇怪的代码,刚好和这个 switch 和 if else 有关!
让我们来看一下这段代码,它属于 ChannelEventRunnable,这个 runnable 是 Dubbo IO 线程创建,将此任务扔到业务线程池中处理
看到没,把 state == ChannelState.RECEIVED 拎出来独立一个 if,而其他的 state 还是放在 switch 里面判断。
我当时脑子里就来回扫描,想想这个到底有什么花头,奈何知识浅薄一脸懵逼。
于是就开始了一波探险之旅!
原来是 CPU 分支预测
现代 CPU 都支持分支预测 (branch prediction) 和指令流水线 (instruction pipeline),这两个结合可以极大提高 CPU 效率。对于简单的 if 跳转,CPU 是可以比较好地做分支预测的。但是对于 switch 跳转,CPU 则没有太多的办法。 switch 本质上是根据索引,从地址数组里取地址再跳转。
也就是说 if 是跳转指令,如果是简单的跳转指令的话 CPU 可以利用分支预测来预执行指令,而 switch 是要先根据值去一个类似数组结构找到对应的地址,然后再进行跳转,这样的话 CPU 预测就帮不上忙了。
然后又因为一个 channel 建立了之后,超过99.9%情况它的 state 都是 ChannelState.RECEIVED,因此就把这个状态给挑出来,这样就能利用 CPU 分支预测机制来提高代码的执行效率。
并且还给出了 Benchmark 的代码,就是通过随机生成 100W 个 state,并且 99.99% 是 ChannelState.RECEIVED,然后按照以下两种方式来比一比(这 benchSwitch 官网的例子名字打错了,我一开始没发现后来校对文章才发现)
至此我们已经知道了这个结论是对的,不过我们还需要深入分析一波,首先得看看 if 和 switch 的执行方式到底差别在哪里,然后再看看 CPU 分支预测和指令流水线的到底是干啥的,为什么会有这两个东西?
条件判断语句是程序的重要组成部分,也是系统业务逻辑的控制手段。重要程度和使用频率更是首屈一指,那我们要如何选择 if 还是 switch 呢?他们的性能差别有多大?switch 性能背后的秘密是什么?接下来让我们一起来寻找这些问题的答案。
switch VS if
我在之前的文章《9个小技巧让你的 if else看起来更优雅》中有提过,要尽量使用 switch 因为他的性能比较高,但具体高多少?以及为什么高的原因将在本文为你揭晓。
我们依然借助 Oracle 官方提供的 JMH(Java Microbenchmark Harness,JAVA 微基准测试套件)框架来进行测试,首先引入 JMH 框架,在 pom.xml 文件中添加如下配置:
然后编写测试代码,我们这里添加 5 个条件判断分支,具体实现代码如下:
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime) // 测试完成时间
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) // 预热 2 轮,每次 1s
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) // 测试 5 轮,每次 1s
@Fork(1) // fork 1 个线程
@State(Scope.Thread) // 每个测试线程一个实例
public class SwitchOptimizeTest {
static Integer _NUM = 9;
public static void main(String[] args) throws RunnerException {
// 启动基准测试
Options opt = new OptionsBuilder()
.include(SwitchOptimizeTest.class.getSimpleName()) // 要导入的测试类
.output("/Users/admin/Desktop/jmh-switch.log") // 输出测试结果的文件
.build();
new Runner(opt).run(); // 执行测试
}
@Benchmark
public void switchTest() {
int num1;
switch (_NUM) {
case 1:
num1 = 1;
break;
case 3:
num1 = 3;
break;
case 5:
num1 = 5;
break;
case 7:
num1 = 7;
break;
case 9:
num1 = 9;
break;
default:
num1 = -1;
break;
}
}
@Benchmark
public void ifTest() {
int num1;
if (_NUM == 1) {
num1 = 1;
} else if (_NUM == 3) {
num1 = 3;
} else if (_NUM == 5) {
num1 = 5;
} else if (_NUM == 7) {
num1 = 7;
} else if (_NUM == 9) {
num1 = 9;
} else {
num1 = -1;
}
}
}
以上代码的测试结果如下:
备注:本文的测试环境为:JDK 1.8 / Mac mini (2018) / Idea 2020.1
从以上结果可以看出(Score 列),switch 的平均执行完成时间比 if 的平均执行完成时间快了约 2.33 倍。
性能分析
为什么 switch 的性能会比 if 的性能高这么多?
这需要从他们字节码说起,我们把他们的代码使用 javac 生成字节码如下所示:
public class com.example.optimize.SwitchOptimize {
static java.lang.Integer _NUM;
public com.example.optimize.SwitchOptimize();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."
4: return
public static void main(java.lang.String[]);
Code:
0: invokestatic #7 // Method switchTest:()V
3: invokestatic #12 // Method ifTest:()V
6: return
public static void switchTest();
Code:
0: getstatic #15 // Field _NUM:Ljava/lang/Integer;
3: invokevirtual #19 // Method java/lang/Integer.intValue:()I
6: tableswitch { // 1 to 9
1: 56
2: 83
3: 61
4: 83
5: 66
6: 83
7: 71
8: 83
9: 77
default: 83
}
56: iconst_1
57: istore_0
58: goto 85
61: iconst_3
62: istore_0
63: goto 85
66: iconst_5
67: istore_0
68: goto 85
71: bipush 7
73: istore_0
74: goto 85
77: bipush 9
79: istore_0
80: goto 85
83: iconst_m1
84: istore_0
85: return
public static void ifTest();
Code:
0: getstatic #15 // Field _NUM:Ljava/lang/Integer;
3: invokevirtual #19 // Method java/lang/Integer.intValue:()I
6: iconst_1
7: if_icmpne 15
10: iconst_1
11: istore_0
12: goto 81
15: getstatic #15 // Field _NUM:Ljava/lang/Integer;
18: invokevirtual #19 // Method java/lang/Integer.intValue:()I
21: iconst_3
22: if_icmpne 30
25: iconst_3
26: istore_0
27: goto 81
30: getstatic #15 // Field _NUM:Ljava/lang/Integer;
33: invokevirtual #19 // Method java/lang/Integer.intValue:()I
36: iconst_5
37: if_icmpne 45
40: iconst_5
41: istore_0
42: goto 81
45: getstatic #15 // Field _NUM:Ljava/lang/Integer;
48: invokevirtual #19 // Method java/lang/Integer.intValue:()I
51: bipush 7
53: if_icmpne 62
56: bipush 7
58: istore_0
59: goto 81
62: getstatic #15 // Field _NUM:Ljava/lang/Integer;
65: invokevirtual #19 // Method java/lang/Integer.intValue:()I
68: bipush 9
70: if_icmpne 79
73: bipush 9
75: istore_0
76: goto 81
79: iconst_m1
80: istore_0
81: return
static {};
Code:
0: iconst_1
1: invokestatic #25 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
4: putstatic #15 // Field _NUM:Ljava/lang/Integer;
7: return
}
这些字节码中最重要的信息是“getstatic #15”,这段代码表示取出“_NUM”变量和条件进行判断。
从上面的字节码可以看出,在 switch 中只取出了一次变量和条件进行比较,而 if 中每次都会取出变量和条件进行比较,因此 if 的效率就会比 switch 慢很多。
提升测试量
前面的测试代码我们使用了 5 个分支条件来测试了 if 和 switch 的性能,那如果把分支的判断条件增加 3 倍(15 个)时,测试的结果又会怎么呢?
增加至 15 个分支判断的实现代码如下:
package com.example.optimize;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime) // 测试完成时间
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) // 预热 2 轮,每次 1s
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) // 测试 5 轮,每次 3s
@Fork(1) // fork 1 个线程
@State(Scope.Thread) // 每个测试线程一个实例
public class SwitchOptimizeTest {
static Integer _NUM = 1;
public static void main(String[] args) throws RunnerException {
// 启动基准测试
Options opt = new OptionsBuilder()
.include(SwitchOptimizeTest.class.getSimpleName()) // 要导入的测试类
.output("/Users/admin/Desktop/jmh-switch.log") // 输出测试结果的文件
.build();
new Runner(opt).run(); // 执行测试
}
@Benchmark
public void switchTest() {
int num1;
switch (_NUM) {
case 1:
num1 = 1;
break;
case 2:
num1 = 2;
break;
case 3:
num1 = 3;
break;
case 4:
num1 = 4;
break;
case 5:
num1 = 5;
break;
case 6:
num1 = 6;
break;
case 7:
num1 = 7;
break;
case 8:
num1 = 8;
break;
case 9:
num1 = 9;
break;
case 10:
num1 = 10;
break;
case 11:
num1 = 11;
break;
case 12:
num1 = 12;
break;
case 13:
num1 = 13;
break;
case 14:
num1 = 14;
break;
case 15:
num1 = 15;
break;
default:
num1 = -1;
break;
}
}
@Benchmark
public void ifTest() {
int num1;
if (_NUM == 1) {
num1 = 1;
} else if (_NUM == 2) {
num1 = 2;
} else if (_NUM == 3) {
num1 = 3;
} else if (_NUM == 4) {
num1 = 4;
} else if (_NUM == 5) {
num1 = 5;
} else if (_NUM == 6) {
num1 = 6;
} else if (_NUM == 7) {
num1 = 7;
} else if (_NUM == 8) {
num1 = 8;
} else if (_NUM == 9) {
num1 = 9;
} else if (_NUM == 10) {
num1 = 10;
} else if (_NUM == 11) {
num1 = 11;
} else if (_NUM == 12) {
num1 = 12;
} else if (_NUM == 13) {
num1 = 13;
} else if (_NUM == 14) {
num1 = 14;
} else if (_NUM == 15) {
num1 = 15;
} else {
num1 = -1;
}
}
}
以上代码的测试结果如下:
从 Score 的值可以看出,当分支判断增加至 15 个,switch 的性能比 if 的性能高出了约 3.7 倍,而之前有 5 各分支判断时的测试结果为,switch 的性能比 if 的性能高出了约 2.3 倍,也就是说分支的判断条件越多,switch 性能高的特性体现的就越明显。
switch 的秘密
对于 switch 来说,他最终生成的字节码有两种形态,一种是 tableswitch,另一种是 lookupswitch,决定最终生成的代码使用那种形态取决于 switch 的判断添加是否紧凑,例如到 case 是 1...2...3...4 这种依次递增的判断条件时,使用的是 tableswitch,而像 case 是 1...33...55...22 这种非紧凑型的判断条件时则会使用 lookupswitch,
tableswitch VS lookupSwitchTest
当执行一次 tableswitch 时,堆栈顶部的 int 值直接用作表中的索引,以便抓取跳转目标并立即执行跳转。也就是说 tableswitch 的存储结构类似于数组,是直接用索引获取元素的,所以整个查询的时间复杂度是 O(1),这也意味着它的搜索速度非常快。
而执行 lookupswitch 时,会逐个进行分支比较或者使用二分法进行查询,因此查询时间复杂度是 O(log n),所以使用 lookupswitch 会比 tableswitch 慢。
接下来我们使用实际的代码测试一下,他们两个之间的性能,测试代码如下:
package com.example.optimize;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime) // 测试完成时间
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) // 预热 2 轮,每次 1s
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) // 测试 5 轮,每次 3s
@Fork(1) // fork 1 个线程
@State(Scope.Thread) // 每个测试线程一个实例
public class SwitchOptimizeTest {
static Integer _NUM = -1;
public static void main(String[] args) throws RunnerException {
// 启动基准测试
Options opt = new OptionsBuilder()
.include(SwitchOptimizeTest.class.getSimpleName()) // 要导入的测试类
.build();
new Runner(opt).run(); // 执行测试
}
@Benchmark
public void tableSwitchTest() {
int num1;
switch (_NUM) {
case 1:
num1 = 1;
break;
case 2:
num1 = 2;
break;
case 3:
num1 = 3;
break;
case 4:
num1 = 4;
break;
case 5:
num1 = 5;
break;
case 6:
num1 = 6;
break;
case 7:
num1 = 7;
break;
case 8:
num1 = 8;
break;
case 9:
num1 = 9;
break;
default:
num1 = -1;
break;
}
}
@Benchmark
public void lookupSwitchTest() {
int num1;
switch (_NUM) {
case 1:
num1 = 1;
break;
case 11:
num1 = 2;
break;
case 3:
num1 = 3;
break;
case 4:
num1 = 4;
break;
case 19:
num1 = 5;
break;
case 6:
num1 = 6;
break;
case 33:
num1 = 7;
break;
case 8:
num1 = 8;
break;
case 999:
num1 = 9;
break;
default:
num1 = -1;
break;
}
}
}
以上代码的测试结果如下:
可以看出在分支判断为 9 个时,tableswitch 的性能比 lookupwitch 的性能快了约 1.3 倍。但即使这样 lookupwitch 依然比 if 查询性能要高很多。
执行效率
我们先简单来个小 demo 看看 if 和 switch 的执行效率,其实就是添加一个全部是 if else 控制的代码, switch 和 if + switch 的不动,看看它们之间对比效率如何(此时还是 RECEIVED 超过99.9%)。
执行结果
来看一下执行的结果如何:
好家伙,我跑了好几次,这全 if 的比 if + switch 强不少啊,所以是不是源码应该全改成 if else 的方式,你看这吞吐量又高,还不会像现在一下 if 一下又 switch 有点不伦不类的样子。
我又把 state 生成的值改成随机的,再来跑一下看看结果如何:
我跑了多次还是 if 的吞吐量都是最高的,怎么整这个全 if 的都是最棒滴。
所以从生成的字节码角度来看 switch 效率应该是大于 if 的,但是从测试结果的角度来看 if 的效率又是高于 switch 的,不论是随机生成 state,还是 99.99% 都是同一个 state 的情况下。
首先 CPU 分支预测的优化是肯定的,那关于随机情况下 if 还是优于 switch 的话这我就有点不太确定为什么了,可能是 JIT 做了什么优化操作,或者是随机情况下分支预测成功带来的效益大于预测失败的情形?
难道是我枚举值太少了体现不出 switch 的效果?不过在随机情况下 switch 也不应该弱于 if 啊,我又加了 7 个枚举值,一共 12 个值又测试了一遍,结果如下:
好像距离被拉近了,我看有戏,于是我背了波 26 个字母,实不相瞒还是唱着打的字母。
扩充了分支的数量后又进行了一波测试,这次 swtich 争气了,终于比 if 强了。
题外话: 我看网上也有对比 if 和 switch 的,它们对比出来的结果是 switch 优于 if,首先 jmh 就没写对,定义一个常量来测试 if 和 switch,并且测试方法的 result 写了没有消费,这代码也不知道会被 JIT 优化成啥样了,写了几十行,可能直接优化成 return 某个值了。
小结一下测试结果
对比了这么多我们来小结一下。
对于热点分支
首先对于热点分支将其从 switch 提取出来用 if 独立判断,充分利用 CPU 分支预测带来的便利确实优于纯 swtich,从我们的代码测试结果来看,大致吞吐量高了两倍。
在热点分支的情形下
而在热点分支的情形下改成纯 if 判断而不是 if + swtich的情形下,吞吐量提高的更多。是纯 switch 的 3.3 倍,是 if + switch 的 1.6 倍。
在随机分支的情形下
在随机分支的情形下,三者差别不是很大,但是还是纯 if 的情况最优秀。
但是从字节码角度来看其实 switch 的机制效率应该更高的,不论是 O(1) 还是 O(logn),但是从测试结果的角度来说不是的。
在选择条件少的情况下 if 是优于 switch 的,这个我不太清楚为什么,可能是在值较少的情况下查表的消耗相比带来的收益更大一些?有知道的小伙伴可以在文末留言。
在选择条件很多的情况下 switch 是优于 if 的,再多的选择值我就没测了,大伙有兴趣可以自己测测,不过趋势就是这样的。
CPU 分支预测
接下来咱们再来看看这个分支预测到底是怎么弄的,为什么会有分支预测这玩意,不过在谈到分支预测之前需要先介绍下指令流水线(Instruction pipelining),也就是现代微处理器的 pipeline。
CPU 本质就是取指执行,而取指执行我们来看下五大步骤,分别是获取指令(IF)、指令解码(ID)、执行指令(EX)、内存访问(MEM)、写回结果(WB),再来看下维基百科上的一个图。
当然步骤实际可能更多,反正就是这个意思需要经历这么多步,所以说一次执行可以分成很多步骤,那么这么多步骤就可以并行,来提升处理的效率。
指令流水线
所以说指令流水线就是试图用一些指令使处理器的每一部分保持忙碌,方法是将传入的指令分成一系列连续的步骤,由不同的处理器单元执行,不同的指令部分并行处理。
就像我们工厂的流水线一样,我这个奥特曼的脚拼上去了马上拼下一个奥特曼的脚,我可不会等上一个奥特曼的都组装完了再组装下一个奥特曼。
当然也没有这么死板,不一定就是顺序执行,有些指令在等待而后面的指令其实不依赖前面的结果,所以可以提前执行,这种叫乱序执行。
我们再说回我们的分支预测。
这代码就像我们的人生一样总会面临着选择,只有做了选择之后才知道后面的路怎么走呀,但是事实上发现这代码经常走的是同一个选择,于是就想出了一个分支预测器,让它来预测走势,提前执行一路的指令。
那预测错了怎么办?这和咱们人生不一样,它可以把之前执行的结果全抛了然后再来一遍,但是也有影响,也就是流水线越深,错的越多浪费的也就越多,错误的预测延迟是10至20个时钟周期之间,所以还是有副作用的。
简单的说就是通过分支预测器来预测将来要跳转执行的那些指令,然后预执行,这样到真正需要它的时候可以直接拿到结果了,提升了效率。
分支预测
分支预测又分了很多种预测方式,有静态预测、动态预测、随机预测等等,从维基百科上看有16种。
静态预测
我简单说下我提到的三种,静态预测就是愣头青,就和蒙英语选择题一样,我管你什么题我都选A,也就是说它会预测一个走势,一往无前,简单粗暴。
动态预测
动态预测则会根据历史记录来决定预测的方向,比如前面几次选择都是 true ,那我就走 true 要执行的这些指令,如果变了最近几次都是 false ,那我就变成 false 要执行的这些指令,其实也是利用了局部性原理。
随机预测
随机预测看名字就知道了,这是蒙英语选择题的另一种方式,瞎猜,随机选一个方向直接执行。
还有很多就不一一列举了,各位有兴趣自行去研究,顺便提一下在 2018 年谷歌的零项目和其他研究人员公布了一个名为 Spectre 的灾难性安全漏洞,其可利用 CPU 的分支预测执行泄漏敏感信息,这里就不展开了,文末会附上链接。
之后又有个名为 BranchScope 的攻击,也是利用预测执行,所以说每当一个新的玩意出来总是会带来利弊。
至此我们已经知晓了什么叫指令流水线和分支预测了,也理解了 Dubbo 为什么要这么优化了,但是文章还没有结束,我还想提一提这个 stackoverflow 非常有名的问题,看看这数量。
为什么处理有序数组要比非有序数组快?
这个问题在那篇博客开头就被提出来了,很明显这也是和分支预测有关系,既然看到了索性就再分析一波,大伙可以在脑海里先回答一下这个问题,毕竟咱们都知道答案了,看看思路清晰不。
就是下面这段代码,数组排序了之后循环的更快。
然后各路大神就蹦出来了,我们来看一下首赞的大佬怎么说的。
一开口就是,直击要害。
You are a victim of branch prediction fail.
紧接着就上图了,一看就是老司机。
他说让我们回到 19世纪,一个无法远距离交流且无线电还未普及的时候,如果是你这个铁路交叉口的扳道工,当火车快来的时候,你如何得知该扳哪一边?
火车停车再重启的消耗是很大的,每次到分叉口都停车,然后你问他,哥们去哪啊,然后扳了道,再重启就很耗时,怎么办?猜!
猜对了火车就不用停,继续开。猜错了就停车然后倒车然后换道再开。
所以就看猜的准不准了!搏一搏单车变摩托。
然后大佬又指出了关键代码对应的汇编代码,也就是跳转指令了,这对应的就是火车的岔口,该选条路了。
后面我就不分析了,大伙儿应该都知道了,排完序的数组执行到值大于 128 的之后肯定全部大于128了,所以每次分支预测的结果都是对了!所以执行的效率很高。
而没排序的数组是乱序的,所以很多时候都会预测错误,而预测错误就得指令流水线排空啊,然后再来一遍,这速度当然就慢了。
所以大佬说这个题主你是分支预测错误的受害者。
最终大佬给出的修改方案是咱不用 if 了,惹不起咱还躲不起嘛?直接利用位运算来实现这个功能,具体我就不分析了,给大家看下大佬的建议修改方案。
最后
这篇文章就差不多了,今天就是从 Dubbo 的一段代码开始了探险之旅,分析了波 if 和 switch,从测试结果来看 Dubbo 的这次优化还不够彻底,应该全部改成 if else 结构。
而 swtich 从字节码上看是优于 if 的,但是从测试结果来看在分支很多的情况下能显示出优势,一般情况下还是打不过 if 。
然后也知晓了什么叫指令流水线,这其实就是结合实际了,流水线才够快呀,然后分支预测预执行也是一个提高效率的方法,当然得猜的对,不然分支预测错误的副作用还是无法忽略的,所以对分支预测器的要求也是很高的。