HikariCP:Dead code elimination优化

  • 关于HikariCP的极致优化,流传文章甚多,我提供一个新角度
  • 阅读本文需要一些JVM、JIT的前置知识
  • 能力有限,敬请斧正

背景介绍

故事开头发生于笔者所在的技术交流群。

A同学提问:有大佬看 HikariCP源码吗?这个我没看懂 X大佬答道:

C2 的时候,调用这个类的任何方法的代码都会被抹掉,相当于没有调用一样。

Y大佬答道:

我印象中用suspend lock的地方既没有const也不是trusted field能去掉么?还是说依赖于类型profile 假设单继承?

X大佬答道:

这里的 static final 相当于 const。 使用的地方如果是可以逃逸的,也有对应的逃逸barrier进行去优化。
两位大佬疯狂输出,我也没闲着,直接git clone一套丝滑小连招,打开源码,定位到相关位置

今天我准备用我浅薄的知识来解释一下,凭什么这么写就可以被"JIT完全优化"。

HikariCP里的前因后果

在HikariCP里那个所谓能够完全优化的类长这个样子:

java 复制代码
public class SuspendResumeLock {

   public static final SuspendResumeLock FAUX_LOCK = new SuspendResumeLock(false) {
   
      @Override
      public void acquire() {}

      @Override
      public void release() {}

      @Override
      public void suspend() {}

      @Override
      public void resume() {}
      
   };
   
}

注意到FAUX_LOCK的所有方法都是空实现,且FAUX_LOCK被static final修饰,JVM启动之后,FAUX_LOCK引用不可变。然后跟随笔者的视角来到使用处: com.zaxxer.hikari.pool.HikariPool#HikariPool

java 复制代码
/* 省略很多代码,只保留文章相关的 */
public final class HikariPool extends PoolBase implements HikariPoolMXBean, IBagStateListener {

    private final SuspendResumeLock suspendResumeLock;

    public HikariPool(final HikariConfig config) {
        /* ... 省略 ... */ 
        boolean allowPoolSuspension = config.isAllowPoolSuspension();
        this.suspendResumeLock = allowPoolSuspension 
            ? new SuspendResumeLock() : SuspendResumeLock.FAUX_LOCK;
        /* ... 省略 ... */ 
    }

}

这里其实不用了解详细逻辑,只需要知道HikariCP有一个配置项,假如你开启这个功能就会走SuspendResumeLock,否则就走上面的空实现。suspendResumeLock也被final修饰,意味着HikariPool构造方法执行suspendResumeLock赋值之后,引用也不可变

其他做法

假如我们用别的实现方式达到目的,比如不在HikariPool实例化的时候就确定suspendResumeLock引用,改造成if else形式

java 复制代码
/* 省略很多代码,只保留文章相关的 */
public final class HikariPool extends PoolBase implements HikariPoolMXBean, IBagStateListener {

    private final SuspendResumeLock suspendResumeLock;

    public HikariPool(final HikariConfig config) {
        /* ... 省略 ... */ 
        boolean allowPoolSuspension = config.isAllowPoolSuspension();
        if (allowPoolSuspension) {
            suspendResumeLock = new SuspendResumeLock();
        }
        /* ... 省略 ... */ 
    }

}

那每次使用的时候都要判断,类似于:

java 复制代码
if (allowPoolSuspension) { 
    suspendResumeLock.acquire(); 
} else {

}

/* 或者 */

if (allowPoolSuspension) { 
    suspendResumeLock.release(); 
} else {
    
}

每次使用都要判断一下是否开启allowPoolSuspension特性,上述只演示了两个方法,其他的不赘述一样的道理。

两者区别

仅从代码的效果上看,平常做法的代码除了不太优雅之外,其实也没啥问题,但是这在HikariCP作者看来存在优化空间。

if else

每次都要多一次判断,而且JIT的优化行为也可能不一样。一旦某个方法被判定为热方法,运行时系统就会请求 JIT 编译器为其生成一个优化版本。因此,人们天真地认为 JIT 会编译方法的全部内容,并将其交给运行时系统。但事实是,允许推测性编译/去优化的运行时系统,让 JIT 能够基于对其行为的一系列假设来编译方法。我们之前在隐式空值检查中见过这种情况。这次,我们将关注一个更普遍的关于冷代码的问题。考虑这个只通过 flag = true 有效调用的方法:

java 复制代码
void m(boolean flag) {
  if (flag) {
     /* do stuff A */
  } else {
     /* do stuff B */
  }
}

即使分析中不知道 flag ,智能 JIT 编译器也可以通过分支分析确定"B"分支从未被取用,并将其编译为:

Java 复制代码
void m() {
  if (condition) {
     /* do stuff A */
  } else {
     /* Assume this branch is never taken */ 
     <trap to runtime system: uncommon branch is taken>
  }
}

因此,实际上不会编译分支 B 中的代码(else分支插入了一个Uncommon Traps)。这节省了编译时间,通常通过避免处理永远不会需要的代码来提高代码密度。请注意,这与基于分支频率的代码布局不同。在这种情况下,如果某个分支的频率正好为零,我们可以完全跳过编译其主体。假如"冷"分支最终被选中时,JVM 将重新编译该方法。

当然本例中的条件(allowPoolSuspension)一旦设定就不可变,我们不用担心会重新编译,但是从现有的资料看,应该是没法把判断指令优化掉,即使每次都是true。(当然有大佬精通JIT知识,且有论据证明会被优化,那我就修改,免得误人子弟

空实现

认真思考的读者应该会有疑问了

java 复制代码
boolean allowPoolSuspension = config.isAllowPoolSuspension();   
this.suspendResumeLock = allowPoolSuspension   
    ? new SuspendResumeLock()   
    : SuspendResumeLock.FAUX_LOCK;

HikariCP的做法虽然少了if else判断,假如我关闭allowPoolSuspension特性,完全没必要调用this.suspendResumeLock的方法。因为SuspendResumeLock.FAUX_LOCK就是一个空实现啊,每次都调用不一样有性能损耗吗?比如源码

java 复制代码
public Connection getConnection(long hardTimeout) throws SQLException {
   suspendResumeLock.acquire();
    
   try {
       /* ...省略... */
   } finally {
      suspendResumeLock.release();
   }
}

可是HikariCP的作者,明明说了这样才能完全优化啊。群里的大佬也说:C2 的时候,调用这个类的任何方法的代码都会被抹掉,相当于没有调用一样。直觉告诉我这里面一定藏着我不知道的知识。

机缘巧合下我得知JVM的DCE优化。死代码优化是JVM JIT编译器的一项重要优化技术,用于识别和消除程序中永远不会被执行或其结果永远不会被使用的代码。这种优化能显著减少生成的机器码大小,提高执行效率。

死代码的类型常见有以下几类:

  1. 不可达代码(Unreachable Code)
java 复制代码
/* 仅作示例,懂意思即可 */
public void exception() {    
    if (true) {       
        throw new IllegalArgumentException();  
    }    
    
    /*死代码*/
    System.out.println("这行代码永远不会执行"); 
}
  1. 无用赋值(Dead Store)
java 复制代码
public int calculate() {
    int x = 10;
    int y = 20;
    /* 如果x后续不再使用,这个赋值就是死代码 */
    x = 30; 
    return y;
}
  1. 无用计算(Dead Computation)
java 复制代码
public void process() {
    /* 如果result从未使用,整个计算都是死代码 */
    int result = expensiveCalculation();
    
    /* 其他不使用result的代码 */
    ... 
}

我们回过头来看看,假如我关闭allowPoolSuspension特性,这个信息是在JVM启动就可得知的,那么局部变量就是SuspendResumeLock.FAUX_LOCK,这个也是可以确定的,且this.suspendResumeLock是被final修饰的,引用不可变,那么JVM就可以放心优化。JVM会认为

java 复制代码
this.suspendResumeLock = SuspendResumeLock.FAUX_LOCK;

然后SuspendResumeLock.FAUX_LOCK全是空操作,什么逻辑也没有,因此如果代码触发JIT优化,完全可以认为此处就是Dead code,因此上文中的getConnection方法可以等效为:

java 复制代码
public Connection getConnection(long hardTimeout) throws SQLException {
    /* 这句代码不存在 */
    suspendResumeLock.acquire();
    
    try {
       /* ...省略... */
    } finally {
       /* 这句代码也不存在 */
       suspendResumeLock.release();
    }
}

这样子的话确实可以跟作者的注释相呼应:

此类实现了一个可用于暂停和恢复池的锁。它还提供了一个伪实现,用于在禁用该功能时使用,希望该功能能够被 JIT 完全"优化"掉

最后

一定有人会问我,你写了这么多到底有啥用,因为这种优化完全就没什么多大的收益,我想用HikariCP作者的自述来回答你: 正是这种明知可不为,而为之的极客精神在感动着我。

这篇文章还有一个很奇怪的名字:Down the Rabbit Hole

Down the Rabbit Hole(掉进兔子洞/掉入了无底洞)。

俗语"Down the Rabbit Hole(掉进兔子洞)"因《爱丽丝梦游仙境》一书而流行。当爱丽丝掉进兔子洞时,她进入了一个不同的世界,那里的一切似乎都不正常。这个俗语用来形容进入一个不寻常的地方或情景之中,或指在网上关注了一个有趣而耗时的话题

哈哈哈,跟着笔者你甚至可以学习到英语,这外语好啊,外语得学......

相关推荐
考虑考虑3 小时前
Jpa使用union all
java·spring boot·后端
bobz9654 小时前
virtio vs vfio
后端
Rexi4 小时前
“Controller→Service→DAO”三层架构
后端
bobz9654 小时前
计算虚拟化的设计
后端
深圳蔓延科技5 小时前
Kafka的高性能之路
后端·kafka
Barcke5 小时前
深入浅出 Spring WebFlux:从核心原理到深度实战
后端
JuiceFS5 小时前
从 MLPerf Storage v2.0 看 AI 训练中的存储性能与扩展能力
运维·后端
大鸡腿同学5 小时前
Think with a farmer's mindset
后端
Moonbit5 小时前
用MoonBit开发一个C编译器
后端·编程语言·编译器