Down the Rabbit Hole
Micro-optimizations 小优化
HikariCP包含很多独立的微小的优化,这些优化几乎都无法评估测量,但是所有小优化一起形成了一个整体的性能提升。其中的一些优化是以几毫秒平摊在数以百万计的调用。
HikariCP contains many micro-optimizations that individually are barely measurable, but together combine as a boost to overall performance. Some of these optimizations are measured in fractions of a millisecond amortized over millions of invocations.
ArrayList
One non-trivial (performance-wise) optimization was eliminating the use of an ArrayList
instance in the ConnectionProxy
used to track open Statement
instances. When a Statement
is closed, it must be removed from this collection, and when the Connection
is closed it must iterate the collection and close any open Statement
instances, and finally must clear the collection. The Java ArrayList
, wisely for general purpose use, performs a range check upon every get(int index)
call. However, because we can provide guarantees about our ranges, this check is merely overhead.
其中一个有意义的优化就是消除在ConnectionProxy
中用于追踪活跃的Statement
对象的ArrayList
。当Statement
关闭了,它必须从集合中删除,并且当整个Connection
关闭了,他必须迭代集合然后关闭所有的Statement
实例,最终清理整个集合对象。java的ArrayList
为了通常的使用,在每个get(int index)
调用时都做了越界检查,但是因为在这里我们可以确保索引范围,所有这个检查是多余的支出。
Additionally, the remove(Object)
implementation performs a scan from head to tail, however common patterns in JDBC programming are to close Statements immediately after use, or in reverse order of opening. For these cases, a scan that starts at the tail will perform better. Therefore, ArrayList
was replaced with a custom class FastList
which eliminates range checking and performs removal scans from tail to head.
另外remove(Object)
的实现是从头到尾扫描数组,然而通常的JDBC编程模型是在使用了Statements之后立即关闭(跟创建statement顺序相反)。因此如果从尾部开始扫描性能会更加好。因此ArrayList
由我们自己定义的一个类FastList
来代替掉了,它消除了索引越界的检查以及在删除时执行从尾到头部的扫描操作。
ConcurrentBag
HikariCP contains a custom lock-free collection called a ConcurrentBag. The idea was borrowed from the C# .NET ConcurrentBag class, but the internal implementation quite different. The ConcurrentBag provides...
A lock-free design 无锁设计
ThreadLocal caching 本地线程缓存
Queue-stealing 工作窃取队列
Direct hand-off optimizations
...resulting in a high degree of concurrency, extremely low latency, and minimized occurrences of false-sharing.
Invocation: invokevirtual
vs invokestatic
简而言之:原先的是单例工厂方法,改成类的静态方法。即原先调用获取代理连接等等的流程是 getStatic 获取单例的工厂对象,然后invokeVirtual调用对象的具体方法来返回代理类。后面改进之后直接invokeStatic调用类的静态方法来返回代理对象,因此性能得到提升。
In order to generate proxies for Connection, Statement, and ResultSet instances HikariCP was initially using a singleton factory, held in the case of ConnectionProxy
in a static field (PROXY_FACTORY).
为了生成Connection, Statement, and ResultSet 的代理对象实例,HikariCP最开始使用了单例工厂方法,在ConnectionProxy
对象的一个静态属性(PROXY_FACTORY)中。
There was a dozen or so methods resembling the following:
项目中有许多如下类似的方法:
public final PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException
{
return PROXY_FACTORY.getProxyPreparedStatement(this, delegate.prepareStatement(sql, columnNames));
}
Using the original singleton factory, the generated bytecode looked like this:
public final java.sql.PreparedStatement prepareStatement(java.lang.String, java.lang.String[]) throws java.sql.SQLException;
flags: ACC_PRIVATE, ACC_FINAL
Code:
stack=5, locals=3, args_size=3
0: getstatic #59 // Field PROXY_FACTORY:Lcom/zaxxer/hikari/proxy/ProxyFactory;
3: aload_0
4: aload_0
5: getfield #3 // Field delegate:Ljava/sql/Connection;
8: aload_1
9: aload_2
10: invokeinterface #74, 3 // InterfaceMethod java/sql/Connection.prepareStatement:(Ljava/lang/String;[Ljava/lang/String;)Ljava/sql/PreparedStatement;
15: invokevirtual #69 // Method com/zaxxer/hikari/proxy/ProxyFactory.getProxyPreparedStatement:(Lcom/zaxxer/hikari/proxy/ConnectionProxy;Ljava/sql/PreparedStatement;)Ljava/sql/PreparedStatement;
18: return
You can see that first there is a getstatic
call to get the value of the static field PROXY_FACTORY
, as well as (lastly) the invokevirtual
call to getProxyPreparedStatement()
on the ProxyFactory
instance.
We eliminated the singleton factory (which was generated by Javassist) and replaced it with a final class having static
methods (whose bodies are generated by Javassist). The Java code became:
public final PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException
{
return ProxyFactory.getProxyPreparedStatement(this, delegate.prepareStatement(sql, columnNames));
}
Where getProxyPreparedStatement()
is a static
method defined in the ProxyFactory
class. The resulting bytecode is:
private final java.sql.PreparedStatement prepareStatement(java.lang.String, java.lang.String[]) throws java.sql.SQLException;
flags: ACC_PRIVATE, ACC_FINAL
Code:
stack=4, locals=3, args_size=3
0: aload_0
1: aload_0
2: getfield #3 // Field delegate:Ljava/sql/Connection;
5: aload_1
6: aload_2
7: invokeinterface #72, 3 // InterfaceMethod java/sql/Connection.prepareStatement:(Ljava/lang/String;[Ljava/lang/String;)Ljava/sql/PreparedStatement;
12: invokestatic #67 // Method com/zaxxer/hikari/proxy/ProxyFactory.getProxyPreparedStatement:(Lcom/zaxxer/hikari/proxy/ConnectionProxy;Ljava/sql/PreparedStatement;)Ljava/sql/PreparedStatement;
15: areturn
There are three things of note here:
-
The
getstatic
call is gone.get static指令消失了
-
The
invokevirtual
call is replaced with ainvokestatic
call that is more easily optimized by the JVM.invokevirtual
被替换成了invokestatic
调用,更加容易被JVM优化。(invokevirtual
需要查询虚方法表来确定方法的直接引用,invokestatic
在类加载的解析阶段就从符号引用转成了直接引用 ) -
Lastly, possibly not noticed at first glance is that the stack size is reduced from 5 elements to 4 elements. This is because in the case of
invokevirtual
there is an implicit passing of the instance of ProxyFactory on the stack (i.ethis
), and there is an additional (unseen) pop of that value from the stack whengetProxyPreparedStatement()
was called.方法stack的深度从5变成了4.这是因为先前的
invokevirtual
调用时需要在stack顶部Pop出一个ProxyFactory实例引用
In all, this change removed a static field access, a push and pop from the stack, and made the invocation easier for the JIT to optimize because the callsite is guaranteed not to change.