引言
在前面 高性能代码优化规则参考
一文中,我们举了一个优化的例子;但是,但是,结果优化变成劣化(恶化了),演砸了。我们继续看看到底是怎么回事?
优化规则评价
先复原一下场景。
光说不练假把式,每种语言运行环境都有各自的表达方式,这里用“最简单“的JavaScript做一个演示。
var i,len,testData = [],testCount = 9999,repeatCount = 999;function doInit(testData){ for(var i = testCount; i--;){ testData[i] = i; }}function doRepeat(operation,data){ for(var i = 0; i < repeatCount;i++){ operation(data); }}function origin(data){ var ret = []; for(var i = 0; i < data.length; i++){ ret[i] = data[i] * data[i]; }}function optLen(data){ var ret = []; for(var i = 0,len = data.length; i < len; i++){ ret[i] = data[i] * data[i]; }}function optIdx(data){ var ret = [],tmpData; for(var i = 0,len = data.length; i < len; i++){ tmpData = data[i]; ret[i] = tmpData * tmpData; }}function optCmp(data){ var ret = []; for(var i = data.length; i-- > 0;){ ret[i] = data[i] * data[i]; }}doInit(testData);console.time("origin")doRepeat(origin,testData);console.timeEnd('origin')console.time('optmize len');doRepeat(optLen,testData)console.timeEnd('optmize len');console.time('optmize index');doRepeat(optIdx,testData)console.timeEnd('optmize index');console.time('optmize compare');doRepeat(optCmp,testData)console.timeEnd('optmize compare');
多次执行上面的代码查看效果,结果如下:
在 Safari 10.1 (12603.1.30.0.34)
下的运行结果(基本稳定):
origin: 163.225msoptmize len: 139.275msoptmize index: 118.999msoptmize compare: 248.611ms
在 Chrome 58.0.3029.96
下的运行结果(不太稳定):
origin: 81.718017578125msoptmize len: 77.73486328125msoptmize index: 74.80517578125msoptmize compare: 378.320068359375ms
在 NodeJS 7.9.0
下的运行结果(不太稳定):
origin: 110.480msoptmize len: 104.123msoptmize index: 98.463msoptmize compare: 383.104ms
上面结果看来,优化循环判断条件和数据缓存都有一定的效果;但最后一种风骚的写法,看起来少了一个 局部变量 ,但执行结果是成倍的劣化(恶化)。
这曾经也是一种代码优化的方式,作者也深中其毒,但这已经不那么重要了。现在问题来了,为什么会这么劣化,是一个值得研究的问题。
问题深入研究
找工具
-
搜索
v8 perf tool
Google 第一条得到一个工具列表 -
筛选掉其中内存和CPU相关的,剩下
irhydra
可以做中间代码分析的 -
我们就是想看看咱们的 源码 被转成什么样的代码来执行了
搞出来看一看
-
node 本身是基于v8的,所以也不用各种下载v8源码编译什么的了,直接上
-
使用node 生成
irhydra
需要的内容 -
将上面的源代码存为
perf-opt.js
-
执行下面的命令
node --trace-hydrogen --trace-phase=Z --trace-deopt --code-comments --hydrogen-track-positions --redirect-code-traces --redirect-code-traces-to=code.asm --print-opt-code perf-opt.js
-
在
IRHydra2
的界面导入上面生成的.asm
和.cfg
文件 -
可以看到我们四个函数分别对应的中间代码了
找文档 捋主线
-
搜索
v8 intermediate representations
在得到的结果中可以得到如下的概念 -
Crankshaft
是v8的编译优化器 -
Hydrogen
是Crankshaft
中的HIR(High-Level intermediate representations)中间代码,更接近于源代码,跟机器无关 -
Lithium
是Crankshaft
中的LIR(Low-Level intermediate representations)中间代码,更接近于机器码,跟机器相关 -
SSA(Static Single Assignment)
是Hydrogen
的描述格式,是一种组织IR(intermediate representations)的方式。 -
搜索
Static Single Assignment
可以得到一些关于SSA的参考 -
参考资料
-
《Compiled Compiler Templates for V8》
-
《Static Single Assignment Book》
-
没有找到更详尽的关于SSA的文档,比如SSA的指令及其含义说明,只有v8源码中的
src/crankshaft/hydrogen-instructions
,如果你找到了,请不吝赐教(怎么找的?)。
中间代码分析
工具介绍
-
Load Compilation Atrifacts
加载.asm
和.cfg
文件 -
IR
标签页,打开眼睛可以看到源代码和中间代码的映射关系,深红色的竖条表示循环范围 -
Graph
标签页,打开问号可以查看控制流图的说明,点击图块可以跳转到IR
标签页
总体结果
-
前三段代码(origin,optLen,optIdx)的控制流图完全一致,B3-B8是循环内容
-
第三段代码(optCmp)与其他三段差别较大,
详细对比
origin 与 optLen的区别
-
origin比optLen在B2块少一个
LoadNamedField t2.%length@24 Smi
获取数组长度,代号i33
,在循环内容之外 -
origin比optLen在B3块多一个
LoadNamedField t2.%length@24 Smi
获取数组长度,代号i42
,每次循环都有这个操作 -
origin比optLen在B5块少一个
LoadNamedField t2.%length@24 Smi
获取数组长度,代号i62
,每次循环都有,这个地方不科学(mark),需要用i62
的地方直接使用i33
就ok -
其他操作无差异
optLen与optIdx的区别
-
optLen比optIdx在B5块多一个
var[4] = t106
赋值,但这个赋值后面好像没用上。。。
optCmp与optLen比较的异常
-
获取data[i]的时候多了
CheckMaps t2 [0x12044e7ace69](stability-check)
操作 -
存储乘积结果的时候多了
CallWithDescriptor t76 t7 t16 t86 t87 s88 t74 #0 changes[*] Tagged
-
异常的描述(这是代码未被编译器优化识别的警告信息)
This instruction has side effects **unknown** to the compiler.It can change hidden classes of objects, kind of array backing stores, etc.Such instructions should be avoided inside hot loops because they inhibit optimizations.
看起来还没有找到想要的
-
上面的
SSA
本身存在问题,参考意义不大 -
没有比较出前面三个代码之间的差异
-
最后一个优化搞砸了还是没解释清楚
重头再来
使用PHP来实现(为什么?)
-
在这个例子上,PHP跟JavaScript极其相似
-
PHP是世界上最好的语言(别打脸~~)
-
PHP下用
VLD
查看opcode
我玩过一次,比SSA
看起来更细致
<?phpdefine('TESTCOUNT',9999);define('REPEATCOUNT',999);define('PRECISION',8);function mstime_str(){ list($usec, $sec) = explode(" ", microtime()); //不丢失系统原有精度 return (bcadd($usec,$sec,PRECISION));}function get_exec_time($func,$param){ $start = mstime_str(); call_user_func(do_repeat, $func,$param); $end = mstime_str(); return bcsub($end,$start,PRECISION);}function do_init(&$arr){ for($i = TESTCOUNT; $i--;){ $arr[$i] = $i; }}function do_repeat($operation,$data){ for($i = 0; $i < REPEATCOUNT;$i++){ $operation($data); }}function origin($data){ $ret = array(); for($i = 0; $i < count($data); $i++){ $ret[$i] = $data[$i] * $data[$i]; }}function opt_len($data){ $ret = array(); for($i = 0,$len = count($data); $i < $len; $i++){ $ret[$i] = $data[$i] * $data[$i]; }}function opt_idx($data){ $ret = array(); for($i = 0,$len = count($data); $i < $len; $i++){ $tmp = $data[$i]; $ret[$i] = $tmp * $tmp; }}function opt_cmp($data){ $ret = array(); for($i = count($data); $i--;){ $ret[$i] = $data[$i] * $data[$i]; }}$test_data = array();do_init($test_data);echo 'origin:'.(get_exec_time(origin,$test_data)).PHP_EOL;echo 'opt_len:'.(get_exec_time(opt_len,$test_data)).PHP_EOL;echo 'opt_idx:'.(get_exec_time(opt_idx,$test_data)).PHP_EOL;echo 'opt_cmp:'.(get_exec_time(opt_cmp,$test_data)).PHP_EOL;?>
执行上面的PHP代码
-
将上面的内容存为
perf-opt.php
-
运行
php -f perf-opt.php
-
在
PHP 5.6.30
上面的执行结果(稳定)origin:4.74019000opt_len:3.53197500opt_idx:3.35585800opt_cmp:2.95980300
结论
-
某些优化规则在不同的编译环境下,不一定能得到一致的效果
-
规则是死的,但编译系统不断在改变和发展
-
有可能我们的优化规则,跟编译器的优化冲突了,成了执行优化的障碍
参考资料
-
《Compiled Compiler Templates for V8》
-
《Static Single Assignment Book》
-
V8 源码
-
github IRHydra