Loading [MathJax]/jax/input/TeX/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Dubbo 这波优化好像不够彻底啊?

Dubbo 这波优化好像不够彻底啊?

作者头像
用户1737318
发布于 2021-09-22 07:11:16
发布于 2021-09-22 07:11:16
32300
代码可运行
举报
文章被收录于专栏:人工智能头条人工智能头条
运行总次数:0
代码可运行

每个时代,都不会亏待会学习的人。

大家好,我是 yes。

这次本来是打算写一篇 RocketMQ 相关文章的,但是被插队了,我也是没想到的,对了本号也有留言了哟。

说来也是巧最近在看 Dubbo 源码,然后发现了一处很奇怪的代码,于是就有了这篇文章,让我们来看一下这段代码,它属于 ChannelEventRunnable,这个 runnable 是 Dubbo IO 线程创建,将此任务扔到业务线程池中处理。

看到没,把 state == ChannelState.RECEIVED 拎出来独立一个 if,而其他的 state 还是放在 switch 里面判断。

我当时脑子里就来回扫描,想想这个到底有什么花头,奈何知识浅薄一脸懵逼。

于是就开始了一波探险之旅!

原来是 CPU 分支预测

遇到问题当然是问搜索引擎了,一般而言我会同时搜索各大引擎,咱这也不说谁比谁好,反正有些时候度娘还是不错的,比如这次搜索度娘给的结果比较靠前,google 较靠后。

一般搜索东西我都喜欢先在官网上搜,找不到了再放开搜,所以先这么搜 site:xxx.com key

你看这就有了,完美啊!

我们先来看看官网的这篇博客怎么说的,然后再详细地分析一波。

Dubbo 官网的博客

现代 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 官网的例子名字打错了,我一开始没发现后来校对文章才发现)。

虽然博客也给出了它的对比结果,但是我还是本地来跑一下看看结果如何,其实 JMH 不推荐在 ide 里面跑,但是我懒,直接 idea 里面跑了。

从结果来看确实通过 if 独立出来代码的执行效率更高(注意这里测的是吞吐),博客还提出了这种技巧可以放在性能要求严格的地方,也就是一般情况下没必要这样特殊做。

至此我们已经知道了这个结论是对的,不过我们还需要深入分析一波,首先得看看 if 和 switch 的执行方式到底差别在哪里,然后再看看 CPU 分支预测和指令流水线的到底是干啥的,为什么会有这两个东西?

if vs switch

我们先简单来个小 demo 看看 if 和 switch 的执行效率,其实就是添加一个全部是 if else 控制的代码, switch 和 if + switch 的不动,看看它们之间对比效率如何(此时还是 RECEIVED 超过99.9%)。

来看一下执行的结果如何:

好家伙,我跑了好几次,这全 if 的比 if + switch 强不少啊,所以是不是源码应该全改成 if else 的方式,你看这吞吐量又高,还不会像现在一下 if 一下又 switch 有点不伦不类的样子。

我又把 state 生成的值改成随机的,再来跑一下看看结果如何:

我跑了多次还是 if 的吞吐量都是最高的,怎么整这个全 if 的都是最棒滴。

反编译 if 和 switch

在我的印象里这个 switch 应该是优于 if 的,不考虑 CPU 分支预测的话,当从字节码角度来说是这样的,我们来看看各自生成的字节码。

先看一下 switch 的反编译,就截取了关键部分。

也就是说 switch 生成了一个 tableswitch,上面的 getstatic 拿到值之后可以根据索引直接查这个 table,然后跳转到对应的行执行即可,也就是时间复杂度是 O(1)。

比如值是 1 那么直接跳到执行 64 行,如果是 4 就直接跳到 100 行。

关于 switch 还有一些小细节,当 swtich 内的值不连续且差距很大的时候,生成的是 lookupswitch,按网上的说法是二分法进行查询(我没去验证过),时间复杂度是 O(logn),不是根据索引直接能找到了,我看生成的 lookup 的样子应该就是二分了,因为按值大小排序了。

还有当 switch 里面的值不连续但是差距比较小的时候,还是会生成 tableswtich 不过填充了一些值,比如这个例子我 switch 里面的值就 1、3、5、7、9,它自动填充了2、4、6、8 都指到 default 所跳的行。

让我们再来看看 if 的反编译结果:

可以看到 if 是每次都会取出变量和条件进行比较,而 switch 则是取一次变量之后查表直接跳到正确的行,从这方面来看 switch 的效率应该是优于 if 的。当然如果 if 在第一次判断就过了的话也就直接 goto 了,不会再执行下面的哪些判断了。

所以从生成的字节码角度来看 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 。

然后也知晓了什么叫指令流水线,这其实就是结合实际了,流水线才够快呀,然后分支预测预执行也是一个提高效率的方法,当然得猜的对,不然分支预测错误的副作用还是无法忽略的,所以对分支预测器的要求也是很高的。

JMH 的测试代码我也打个包,想自己跑的同学后台输入「分支预测」即可获取,如果觉得这篇文章不错点个在看哟,如果有纰漏请赶紧联系鞭挞我。

巨人的肩膀

Spectre :https://www.freebuf.com/vuls/160161.html

Dubbo 博客 :http://dubbo.apache.org/zh-cn/blog/optimization-branch-prediction.html

https://stackoverflow.com/questions/11227809/why-is-processing-a-sorted-array-faster-than-processing-an-unsorted-array

https://en.wikipedia.org/wiki/Instruction_pipelining

https://en.wikipedia.org/wiki/Branch_predictor

代码语言:javascript
代码运行次数:0
运行
复制
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-09-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 人工智能头条 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
【揭秘】为什么程序员喜欢用大量的if else而偏不用switch!
说来也是巧最近在看 Dubbo 源码,然后发现了一处很奇怪的代码,刚好和这个 switch 和 if else 有关!
Java程序猿
2021/04/05
7550
一文告诉你CPU分支预测对性能影响有多大
来源于stackoverflow上的一个问题为什么处理有序数组比处理无需数组快,原文中已经有了一些探讨,这里我们首先来复现下结果,然后再解释下为什么!
xindoo
2021/01/21
1.7K0
Rust为什么放弃Switch结构
今天我们还是继续来聊高并发的话题,我们知道Swich分支是一个非常有用的语法,这是一个可以回溯到上世纪的Pascal、C等经典语言的分支结构,主要的作用就是判断变量的取值并将程序代码送入不同的分支,这种设计在当时的环境下非常的精妙,但是在当前最新的CPU环境下,却会带来很多意想不到的坑。
beyondma
2021/08/14
1.1K0
虽然是高手过招的杀手锏,但是对写业务代码没有任何卵用。
这篇文章给大家盘一下“分支预测”这个听起来玄乎,但是对写业务代码没有任何卵用的小技巧。
why技术
2024/03/18
2050
虽然是高手过招的杀手锏,但是对写业务代码没有任何卵用。
全网最详细的Intel CPU体系结构分析(内核源码)
前段meldown漏洞事件的影响,那段时间也正好在读Paul的论文关于内存屏障的知识,其中有诸多细节想不通,便陷入无尽的煎熬和冥想中,看了《计算机系统结构》、《深入理解计算机系统》、《大话处理器》等经典书籍,也在google上搜了一大堆资料,前前后后、断断续续地折腾了一个多月,终于想通了,现在把自己的思想心得记录下来,希望对有这方面困惑的朋友有些帮助。
嵌入式Linux内核
2022/09/22
1.1K0
全网最详细的Intel CPU体系结构分析(内核源码)
手机遇到性能BUG怎么破?
目前手机SOC的性能越来越少,很多程序员在终端程序的开发过程中也不太注意性能方面的优化,尤其是不注意对齐和分支优化,但是这两种问题一旦出现所引发的问题,是非常非常隐蔽难查的,最终几个问题得到了圆满解决。
beyondma
2021/10/23
4900
处理器结构--分支预测(Branch Prediction)
条件分支指令通常具有两路后续执行分支。即不采取(not taken)跳转,顺序执行后面紧挨JMP的指令;以及采取(taken)跳转到另一块程序内存去执行那里的指令。是否条件跳转,只有在该分支指令在指令流水线中通过了执行阶段(execution stage)才能确定下来。
None_Ling
2018/10/24
3.4K0
处理器结构--分支预测(Branch Prediction)
远看像乱序执行,近看是内存屏障的BUG是如何被解决的
前几天我发布了《Serverless时代Rust将迎春天》后,针对热心读者的回复针对他所提出的问题我又总结了一些文章,其中我对于多并发操作,结果却还是0的情况给出了多核竞争冲突的解释,结果一石击起千层浪,再次收到很多热心读者的反馈,其中有几个回复特别值得一说。
beyondma
2021/07/11
7180
远看像乱序执行,近看是内存屏障的BUG是如何被解决的
Java volatile修饰符的用法及作用详解版
1、               内存访问操作/指令执行操作的乱序:假设每个CPU都分别运行着一个会触发内存访问操作的程序。那么对于这样一个CPU,其内存访问顺序是非常松散的,在保证程序上下文逻辑关系的前提下,CPU可能乱序执行内存操作。此外,编译器也可以将它输出的指令安排成任何它喜欢的顺序,只要保证不影响程序表面的执行逻辑。这里就涉及到了两次可能发生指令重排的情况:一个是编译的时候,由编译原理的知识知道,编译器会对代码进行优化,这一步就涉及到指令重排,当然,编译完成之后的目标代码中指令的顺序就是确定的,不同线程执行该代码的顺序是一样的;另一个就是CPU在执行具体的指令的时候,也会因为计算机当前的状态(比如寄存器的占用情况、ALU的使用情况,cup缓存层的存在等原因)的不同导致指令最终的执行顺序发生变化(实际上,cpu本身并不会对指令进行重排,它本身是按照编译后的顺序来执行指令的,只是由于执行不同的指令需要的时间长短不同,以及缓存层的存在,再加上CPU执行指令的流水线并不是串行化等因素,那么就有可能出现排在靠前位置的指令还没执行完,而排在靠后的指令已经执行完了的情况,这一情况就是所谓的CPU执行指令的乱序,具体原因后面会更详细地解释),尽管这个变化可能不影响最终结果的正确性。
saintyyu
2021/11/22
6030
Java volatile修饰符的用法及作用详解版
计算机指令的流水线执行与流水线冒险
不要等待计算结果保存到目的寄存器,增加一条额外数据通路,将计算的结果直接传给下一条指令计算的输入
叶茂林
2023/11/19
5310
计算机指令的流水线执行与流水线冒险
从洗衣机的故事中理解计算机的工作原理:优化指令执行速度
CPU执行一条指令也是类似的操作:取址-》解码-》执行,不断重复。此时一条指令需要三个时钟周期才能完成(取址,解码,执行)。
北洋
2023/09/22
2990
CPU的价值
一种提升性能的方法叫 "指令流水线",想象下你要洗一整个酒店的床单,但只有 1 个洗衣机, 1 个干燥机。
硬核编程
2019/08/19
7330
C++大厂面试真题宝典 精选100道
通过汇编视角分析,可以直观看到优化如何影响指令流水线、内存访问模式和CPU资源利用率,这是理解性能瓶颈的关键。
用户11659095
2025/05/30
540
CPU摸鱼被抓,上了一个新技术!
我叫阿Q,是CPU一号车间里的员工,我所在的这个CPU足足有8个核,就有8个车间,干起活来杠杠滴。
轩辕之风
2023/09/05
1700
CPU摸鱼被抓,上了一个新技术!
少写点if-else吧,它的效率有多低你知道吗?
我要再和生活死磕几年。要么我就毁灭,要么我就注定铸就辉煌。如果有一天,你发现我在平庸面前低了头,请向我开炮。
C语言与CPP编程
2021/03/25
9860
少写点if-else吧,它的效率有多低你知道吗?
CPU性能分析与优化(二)
大多数现代体系结构可以归类于基于寄存器的loadstore架构,其中操作数明确指定,内存只能通过load/store来访问。除了基本的load store control 标量算术操作(使用整数和浮点型),当前ISA还在增加新的计算模式。vector处理 Intel整了AVX系列,ARM整了SVE,RISC-V整了V extension,matrix/tensor Intel还整了AMX。通常使用这些高级指令,软件的速度会有数量级的提升。
王很水
2024/08/06
3230
CPU性能分析与优化(二)
Canvas CPU漏洞武器化工具大揭秘
外部有报道称出现了CPU漏洞spectre的在野利用,各项证据均指向为Immunity Canvas商业渗透框架里面的spectre CPU漏洞攻击模块被上传到了VirusTotal,由此揭开了spectre武器化工具的神秘面纱。2018年披露的spectre和meltdown CPU漏洞影响范围广泛,但由于修复困难、修复方案影响性能、没有出现武器化工具而一直被轻视。恰巧Blade Team在CPU漏洞方面有一些研究和积累,有幸获得一份Immunity Canvas,于是开始对spectre武器化工具抽丝剥茧,一探究竟,希望能够引起大家对CPU漏洞的重视。
腾讯安全应急响应中心
2021/03/15
5760
Canvas CPU漏洞武器化工具大揭秘
为什么处理排序的数组要比非排序的快
以下是c++的一段非常神奇的代码。由于一些奇怪原因,对数据排序后奇迹般的让这段代码快了近6倍!!
云扬四海
2019/06/05
5380
为什么处理排序的数组要比非排序的快
浅谈CPU 并行编程和 GPU 并行编程的区别
CPU 的并行编程技术,也是高性能计算中的热点,那么它和 GPU 并行编程有何区别呢? 本文将做出详细的对比,分析各自的特点,为深入学习 CPU 并行编程技术打下铺垫。 区别一:缓存管理方式的不同 •GPU:缓存对程序员不透明,程序员可根据实际情况操纵大部分缓存 (也有一部分缓存是由硬件自行管理)。 •CPU:缓存对程序员透明。应用程序员无法通过编程手段操纵缓存。 区别二:指令模型的不同 • GPU:采用 SIMT - 单指令多线程模型,一条指令配备一组硬件,对应32个线程 (一个线程束)。 • CPU:采
企鹅号小编
2018/02/01
1.5K0
人生充满选择,编程也是
人生在世,我们每天都需要进行三项重大选择:早餐吃什么,午餐吃什么,晚餐吃什么。这一度让我感到非常为难,于是我养成了一个习惯,只要在附近的餐馆发现了几种还不错的食物,我就会连续一段时间一直吃它们,直到吃腻,再尝试一下别的选择,直到又发现还比较对胃口的,就再一直吃,如此循环往复。
Sheepy
2018/09/10
6240
相关推荐
【揭秘】为什么程序员喜欢用大量的if else而偏不用switch!
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档