前段时间做一些小性能优化相关的东西.
然后想起了go的benchmark工具.
起因是spark/scala的一个json解析的micro benchamrk问题.
一个utf16转ut8的过程总是有着不到7%左右的差距.
后来同事提醒一个allocation的细节才明白缘由.
大致就是特定test case下,一个缓存开启/命中带来的有没有新allocation的一个副作用.
想到go是因为想起go的benchmark,相比jmh的throughput模式,还多了alloc的相关统计.
大概这就是所谓的工程细节.
成熟度和经验的一些不显著的不易被察觉的点.
如果对应的jmh有相关指标的话,可能就比较一目了然了.
然后其他的也是一些micro或者说对应scale范围下才显现的问题.
一个是scala的type check和类型推导的问题.
代码和语法层面的显示typecheck/cast带来的安全性comfortable的代价是一些runtime时候的性能损失,根据写法的不同有些还是不是neglectable的.
而换一些写法让compiler happy地能够做出type inference的,可以把这部分的开销抹去.
而比较讽刺的是本质上来说,由于类型擦除这个jvm特性,两者在某种程度上的逻辑其实都是object到处裸奔.
区别只不过是runtime check还是compile time check罢了.
这时候也就又些理解c++的一些复杂语法和奇技淫巧了.
就像理论上来说,都是一回事.
但工程实现的不同,可能就是所谓的差距了.
类似的还有scala functional风格的另一面.
一个foreach里buffer append的风格和一个纯map/flatmap的风格在性能上的hotspot也是有些差异的.
当然,这部分差异可能来源于对结果取值的处理场景.
毕竟一定程度上来说,后者是个compress的lazy evaluator.
在只需要特定subset元素的时候,确实会有一定程度上的作弊效果.
因为只需要evaluate一部分就行了.
这点倒是想起来以前pytorch跟tensorflow的设计差异了.
后者基于graph的lazy特定也让前者的所见即得带来习惯上的使用冲突.
毕竟lazy对于debug print有时候还是不太友好的.
尤其一定程度上来说,语义是不等价的.
其他的一些就是更low level的可观测性问题了.
就像开头的allocation的问题.
后面case by case地看了下,有显著性能差异的时候还是buffer比较大的时候的情况.
一个稍微简单的intuition解释大概就是small allocation在runtime可能更容易被更底层的memory management的cache命中,可能是gc的一些特殊case,甚至malloc的一些具体实现都可能对small allocation可以更容易地被reuse/命中.
而稍微大点的allocation则很容易命中到page fault从而引发一些application层面看不到的更重的差异调用链了.
这个在latency到micro second层面就算比较显著了.
所有反过来说,如果只是毫秒级,那可能这方面的差异优化又显得没那么重要和必要了.
这也就是前面说的scale的问题.
另外一个相关的就是cache这个概念的意义的问题了.
比较初期的prototype的时候,也考虑过加一些cache policy.
直接用guava的结果出来之后自己都笑了.
后来手写了一些简单策略看效果也并没有特别显著的提升.
本质上来说,还是scale的问题.
因为caceh逻辑可能在复杂度/开销方面跟主逻辑已经具有相当的可比性了.
所以即使在命中的场景下,能明显地提提升,但回退到miss的时候,回退可能更显著.
这也就是有时候会开始觉得,cache并不难算是一种算法层面的优化方式.
或者说它是一种比较speculate的优化选择.
形式上来说,天然就带有某种不确定性.
剩下的一个题外话就是关于内存数据结构layout的问题了.
以前刚接触Arrow的时候也颇为嗤之以鼻.
觉得算是可以有,但意义没宣传得那么大.
现在觉得,就像utf16转utf8这种.
本质上来说还是一位representation不一致引入的额外routine.
类似的还有jni接口带来的overhead.
这些借用一个名字就是non-unify memory access带来的代价.
所以如果像Arrow或者其他adoption比较广的message格式,多多少少还是能避免这种也算是碎片化带来的问题的.
当然,剩下的问题可能就是都想解决碎片化带来的进一步碎片化问题.
这可能就是工程化的另一面.
可观测性带来的避免重复性驱动,然后就是新的工具链在试图更广而全地解决问题.
也就是所谓的silver bullet构想.
比较现实的例子可能就是堪称日新月异的前端工具链生态.
可能隔一两个月就有一套新的概念和东西出来.
然后相似的就是不管实现目标如何,最后还是要兼容老的一套.
在生态和工程化创新之间举步维艰.
于是在回头看褒贬不一的对于go大道至简的哲学揶揄可能就显得不那么充分了.
可能它的工程化实践的已经是考虑了历史债务的成本问题了.
毕竟之前尝试重构某东西的时候也发觉了.
有时候重写本身并不难,难的可能是兼容性/一致性问题.
老旧系统/工具链的价值一方面固然是能work.
另一方面更多的是对于现有使用场景的覆盖度问题.
尤其有些时候一些bug可能变成了某种形式的feature.
以及有些衍生工具链和平台已经依赖了某些难以分类为bug还是feature的东西.
就像一个社会系统.
纷繁链条,你也很难评价一个切面的功过是非.
能用就行可能是一种需要特别角度去看待的工程哲学.
没有评论:
发表评论