2019-11-16

JIT Inline的一些问题

大致这么一段逻辑
```
void test(item){
if(array_list == null){
array_list = new ArrayList<>();
}

if(!array_list.contains(item)) {
array_list.add(item);
}
}
```
这段代码在两台机器和不同用户之间会有些比较明显的性能差异.

具体是有两台机器A,B.
A为一台KVM,B是物理机器.
A宿主机器跟B是同型号CPU.

现象是一个benchmark在A上大概是5分钟不到.
而B是20-30分钟左右.

另外一个就是在B上以root和非root用户偶尔也会有些可见差别.

这个case的实际逻辑是是加载并解析一个配置,并且是单线程的.
所以整体过程应该是deterministic的.
理论上不应该有这么明显的差异的.

perf了一下.
B上面有一段是change_protection_range会相对显著地跟A有所区别.

大概翻了下对应内核版本的实现,看上去有一些huge page/large page相关的东西.
但是看系统参数和JVM配置,largepage相关的选项也并没有打开.

而且对比了下两者的环境变量和生效配置以及相应的.so也是一致的.

所以从系统层面是上来说,应该都是没差别的.

于是看了下VMThread的safepoint信息.

从日志里看是有一些bias lock的revoke情况.
但是从代码逻辑上来说并没有显著的synchronized应用.
这点就有些奇怪了.

把bias lock disable之后greys attach上去做profile.
然后发觉貌似性能是上去了.
两边都差不多是每秒20-30w左右的调用次数.

但是感觉不是很有说服力.

一个想法是bias lock带来的一点object的开销在这种频繁可能会有新对象创建的场景下被放大.
这样的话可能会跟perf的change_protection_range有联动.
因为可能会对cacheline有影响,毕竟一个是KVM一个是物理机器.

为了确定关联性,把profile去掉之后发觉性能差异有出现了.

那么为什么profile会影响到性能呢?
而且是positive的影响.

想起agent在retransform的时候,redefine class会触发deoptimize.

那么,如果是JIT的问题的话,agent deoptimize能提升性能,也就意味着触发了某个优化规则导致的.

尝试不同的compilation policy.
发现在non-tiered compilation(simple/stack walk)的情况下是ok的.
但是开启tiered compilation(simple threshold/advance threshold)之后则有大幅下降.

用perf-map-agent重新perf看了下.
发现non-tiered的情况下,hot code是array list的contains/indexOf.
而tiered的则是test和equals.

到这里其实就已经很明显了.

non-tiered只inline array list的相关调用,包括indexOf里的equals.
所以perf只看到了contains或者indexOf.

而tiered JIT了test和equals.

实际上看生产的assembly,除了做inline之外,同时还对indexOf做了loop unroll.

但是看perf的结果是test和equals同时被JIT了.
那么也就意味着test没有把equals inline进去.

看JIT生产的assembly证实了,对于整个调用链,也就是indexOf里的equals还是callq调用的.

所以应该是某个机制限制了把equals inline进去.
从而loop unroll之后反而引入了函数调用开销.

把inline日志打开可以看到test的inline有两类信息.
一个是callee is too large.
另外一个是already compiled into a big method.

这就可以解释了.

因为test本身不复杂,所以没有触发自身被caller inline进去.
于是hot code也只会inline这个调用链以下的.
而由于inline和loop unroll使得本身的code size变得比较大,在尝试进一步inline equals的时候被拒绝了.

调整这两个相关参数之后再看确实没有了equals的调用,同时性能保持了一致性.

而再回头看为什么KVM和物理机会有性能差异的时候,大概就是因为compiler thread的数量和并发程度不同.
因此在尝试inline equals的时候会出现时机和结果的不一致性.

所以想想的话,从代码风格上来说,确实compact和reusable的会相对来说比较友好一点.

像Stream这种batch process的风格来说的话,确实有可能for之类的手工展开的好.
因为一个loop unroll,一个是函数主题一般都比较小.
再就是手工展开的话,很难说不会触发像上面这种inline不完整反而受累的情况.












没有评论:

发表评论

爽文

去看了好东西. 坦白说,多少是带着点挑刺的味道去的. 毕竟打着爱情神话和女性题材的气质,多多少少是热度为先了. 看完之后倒是有些新的想法. 某种程度上来说,现在的年轻人或者说声音就像小叶. 只要说点贴心的话就能哄好. 也是那种可以不用很努力了. 留在自己的舒适区避难所小圈子抱团就...