HikariCP-最速のJava接続プール

Javaは最近20歳になりました。 今日はすべてがJavaで書かれているように思われます。 Javaのアイデア、プロジェクト、ツールはありますか? -それはすでにです。 特に、世界中の何百万人もの開発者によって使用されているデータベースへの接続のプールのような些細なことに関しては。 しかし、そこにありました! 会いましょう-HikariCPプロジェクトは、これまでで最速のJava接続プールです。



HikariCPは、何百万人もの人々が使用し、数十年生きてきたとしても、一部のソリューションの有効性を常に疑う価値があるという事実の別の顕著な例です。 ひかりは、単独では単独で0.00001%を超える成長をもたらすことのできないマイクロ最適化が、非常に高速で効果的なツールの作成を可能にする優れた例です。



この投稿は、HikariCPによるDown the Rabbit Holeの記事を私の心の流れと混ぜて無料で部分的に翻訳したものです。



画像







ウサギの穴を下る





この記事は、私たちの秘密のソースのレシピです。 あらゆる種類のベンチマークを検討し始めるとき、あなたは、普通の人のように、それらに対して健全な懐疑心を持つべきです。 パフォーマンスと接続プールについて考えると、プールが最も重要な部分であるという陰湿な考えを避けることは困難です。 実際、これは完全に真実ではありません。 他の一般的なJDBC操作と比較したgetConnection()呼び出しの数はかなり少ないです。 ConnectionStatementなどのラッパーを最適化することにより、膨大な数のパフォーマンスの改善が達成されます。



HikariCPを(現状のまま)高速にするために、バイトコードレベル以下まで掘り下げる必要がありました。 JITを支援するために、私たちが知っているすべてのトリックを使用しました。 各メソッドのコンパイル済みバイトコードを調査し、メソッドをインライン化の制限を下回るように変更しました。 継承のレベル数を減らし、変数のスコープを減らすために一部の変数へのアクセスを制限し、型変換を削除しました。

ときどき、メソッドがインライン化の制限を超えているのを見て、数バイトの命令を取り除くように変更する方法を考えました。 例:



public SQLException checkException(SQLException sqle) { String sqlState = sqle.getSQLState(); if (sqlState == null) return sqle; if (sqlState.startsWith("08")) _forceClose = true; else if (SQL_ERRORS.contains(sqlState)) _forceClose = true; return sqle; }
      
      







接続損失エラーがあるかどうかを確認するかなり簡単な方法。 そして今、バイトコード:



 0: aload_1 1: invokevirtual #148 // Method java/sql/SQLException.getSQLState:()Ljava/lang/String; 4: astore_2 5: aload_2 6: ifnonnull 11 9: aload_1 10: areturn 11: aload_2 12: ldc #154 // String 08 14: invokevirtual #156 // Method java/lang/String.startsWith:(Ljava/lang/String;)Z 17: ifeq 28 20: aload_0 21: iconst_1 22: putfield #144 // Field _forceClose:Z 25: goto 45 28: getstatic #41 // Field SQL_ERRORS:Ljava/util/Set; 31: aload_2 32: invokeinterface #162, 2 // InterfaceMethod java/util/Set.contains:(Ljava/lang/Object;)Z 37: ifeq 45 40: aload_0 41: iconst_1 42: putfield #144 // Field _forceClose:Z 45: aload_1 46: return
      
      







Hostpot JVMのインライン制限が35バイトコードの命令であることは、おそらく誰にも秘密ではありません。 そのため、この方法を減らすためにこの方法に注意を払い、次のように変更しました。



 String sqlState = sqle.getSQLState(); if (sqlState != null && (sqlState.startsWith("08") || SQL_ERRORS.contains(sqlState))) _forceClose = true; return sqle;
      
      







限界にかなり近づいていますが、それでも36命令です。 したがって、これを行いました。



 String sqlState = sqle.getSQLState(); _forceClose |= (sqlState != null && (sqlState.startsWith("08") || SQL_ERRORS.contains(sqlState))); return sale;
      
      







簡単に見えます。 本当じゃない? 実際、このコードは以前のコード(45命令)よりも悪いです。

別の試み:



 String sqlState = sqle.getSQLState(); if (sqlState != null) _forceClose |= sqlState.startsWith("08") | SQL_ERRORS.contains(sqlState); return sqle;
      
      







単項OR(|)の使用に注意してください。 これは、実際のパフォーマンスのために(メソッドがインラインになるため)理論的なパフォーマンスを犠牲にする素晴らしい例です(理論的には||より高速になります)。 結果バイトコード:



 0: aload_1 1: invokevirtual #153 // Method java/sql/SQLException.getSQLState:()Ljava/lang/String; 4: astore_2 5: aload_2 6: ifnull 34 9: aload_0 10: dup 11: getfield #149 // Field forceClose:Z 14: aload_2 15: ldc #157 // String 08 17: invokevirtual #159 // Method java/lang/String.startsWith:(Ljava/lang/String;)Z 20: getstatic #37 // Field SQL_ERRORS:Ljava/util/Set; 23: aload_2 24: invokeinterface #165, 2 // InterfaceMethod java/util/Set.contains:(Ljava/lang/Object;)Z 29: ior 30: ior 31: putfield #149 // Field forceClose:Z 34: return
      
      







35バイトコード命令の制限のすぐ下。 これは小さな方法であり、実際にはあまり負荷がかかりませんが、あなたはその考えを理解しました。 小さなメソッドでは、JITでコードを埋め込むことができるだけでなく、実際のマシン命令が少なくなるため、プロセッサのL1キャッシュに収まるコードの量が増えます。 ライブラリ内のこのような変更の数をすべてこれに掛けると、HickaryCPが本当に速い理由を理解できます。



マイクロ最適化





HikariCPには多くのマイクロ最適化があります。 それとは別に、彼らは確かに写真を作りません。 しかし、すべてを合わせると、全体的な生産性が大幅に向上します。 これらの最適化の一部は、数百万のコールのマイクロ秒の小数部です。



配列リスト





最も重要な最適化の1つは、開いているStatementオブジェクトを追跡するために使用されたConnectionProxyクラスのArrayList <Statement>コレクションを削除することでした。 Statementが終了したら、このコレクションから削除する必要があります。 また、接続が閉じている場合は、コレクションを調べて開いているステートメントをすべて閉じてから、コレクションをクリアする必要があります。 ご存知のように、 ArrayListget(index)の呼び出しごとにインデックスの範囲をチェックします。 ただし、正しいインデックスの選択を保証できるため、このチェックは不要です。 また、 remove(Object)メソッドの実装は、リストの最初から最後まで渡されます。 同時に、JDBCで一般に受け入れられているパターンは、使用直後にステートメントを閉じるか、逆順(FILO)にすることです。 そのような場合、リストの最後から始まるパッセージが高速になります。 したがって、 ArrayList <Statement>を、範囲のチェックがなく、リストからのアイテムの削除が最後から始まるFastStatementListに置き換えました。



遅いシングルトン





ConnectionStatementResultSet HikariCPオブジェクトのプロキシを生成するために、もともとシングルトンファクトリを使用していました。 たとえば、 ConnectionProxyの場合、このファクトリは静的フィールドPROXY_FACTORYにありました 。 また、コードには、このフィールドを参照する数十の場所がありました。



 public final PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException { return PROXY_FACTORY.getProxyPreparedStatement(this, delegate.prepareStatement(sql, columnNames)); }
      
      







バイトコードでは、次のようになりました。



 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
      
      







静的フィールドPROXY_FACTORYの値を取得するために、 getstatic呼び出しが最初に来ることがわかります。 また、 ProxyFactoryオブジェクトのgetProxyPreparedStatement()メソッドの最後のinvokevirtual呼び出しにも注意してください

最適化は、シングルトンファクトリを削除し、静的メソッドを含むクラスに置き換えたことです。 コードは次のようになり始めました。



 public final PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException { return ProxyFactory.getProxyPreparedStatement(this, delegate.prepareStatement(sql, columnNames)); }
      
      







ここで、 getProxyPreparedStatement()ProxyFactoryクラスの静的メソッドです。 そして、これがバイトコードです:



 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
      
      







ここでは、すぐに3つのポイントに注意を払う必要があります。 getstatic呼び出しはなくなりました。 invokevirtualは invokestaticに置き換えられました 。これは、仮想マシンにより最適化されています。 最後に気づきにくいポイント-スタックのサイズが5要素から4に減少しました。 最適化の前に、 invokevirtualの場合、 ProxyFactoryオブジェクト自体へのリンクもスタックに到達する必要があります 。 これは、 getProxyPreparedStatement()が呼び出されたときにスタックからこのリンクを取得するための追加のポップ命令も意味します。 一般に、要約すると、静的フィールドへのアクセスを取り除き、スタック上の不要なプッシュおよびポップ操作を削除し、メソッド呼び出しをJIT最適化により適したものにしました。



終わり。



オリジナルダウンザラビットホールを完了してください。



更新:

コメントでは、「Slow Singleton」の記事が多くの議論を引き起こしました。 apanginは、これらのマイクロ最適化はすべて無意味であり、 利益をもたらさないと主張しています。 この解説は、同じ値invokeVirtualinvokeStaticの単純なベンチマークを提供します。 そして、 ここにクラスメートの接続プールのベンチマークがあります。これはおそらくHickaryCPより4倍高速です。 これに対して、HickaryCPの作成者は次の答えを示します



最初に、@ odnoklassnikiのコメントについてコメントしたいと思います。彼らのプールは4倍高速です。 そのプールをJMHベンチマークに追加し、誰でも実行できるように変更をコミットしました。 結果は次のとおりです。 HikariCP:



 ./benchmark.sh clean quick -p pool=one,hikari ".*Connection.*" Benchmark (pool) Mode Cnt Score Error Units ConnectionBench.cycleCnnection one thrpt 16 4991.293 ± 62.821 ops/ms ConnectionBench.cycleCnnection hikari thrpt 16 39660.123 ± 1314.967 ops/ms
      
      







これは、 one-datasourceの 8倍の速度でHikariCPを示しています



Wikiページが作成されてからHikariCPが変更されただけでなく、JMHテストハーネス自体も変更されたことに留意してください。 その時に得た結果を再現するために、その特定のコミットでHikariCPソースをチェックアウトし、そのコミットの直前にソースをチェックアウトしました。 私はその時に利用可能なベンチマークハーネスを使用して両方を実行しました。



静的プロキシファクトリメソッドの前:

 Benchmark (pool) Mode Samples Mean Mean error Units ConnectionBench.testConnectionCycle hikari thrpt 16 9303.741 67.747 ops/ms
      
      







静的プロキシファクトリメソッドの後:

 Benchmark (pool) Mode Samples Mean Mean error Units ConnectionBench.testConnectionCycle hikari thrpt 16 9436.699 71.268 ops/ms
      
      







変更後の平均誤差を上回るわずかな改善が見られます。



通常、すべての変更はコミットされる前にベンチマークでチェックされるため、ベンチマークが改善を示さない限り、その変更をコミットしていたかどうかは疑わしいです。



編集:そして、2014年1月以降、HikariCPのパフォーマンスが向上しました!



All Articles