Javaは最近20歳になりました。 今日はすべてがJavaで書かれているように思われます。 Javaのアイデア、プロジェクト、ツールはありますか? -それはすでにです。 特に、世界中の何百万人もの開発者によって使用されているデータベースへの接続のプールのような些細なことに関しては。 しかし、そこにありました! 会いましょう
-HikariCPプロジェクトは、これまでで最速のJava接続プールです。
HikariCPは、何百万人もの人々が使用し、数十年生きてきたとしても、一部のソリューションの有効性を常に疑う価値があるという事実の別の顕著な例です。 ひかりは、単独では単独で0.00001%を超える成長をもたらすことのできないマイクロ最適化が、非常に高速で効果的なツールの作成を可能にする優れた例です。
この投稿は、HikariCPによる
Down the Rabbit Holeの記事を私の心の流れと混ぜて無料で部分的に翻訳したものです。

ウサギの穴を下る
この記事は、私たちの秘密のソースのレシピです。 あらゆる種類のベンチマークを検討し始めるとき、あなたは、普通の人のように、それらに対して健全な懐疑心を持つべきです。 パフォーマンスと接続プールについて考えると、プールが最も重要な部分であるという陰湿な考えを避けることは困難です。 実際、これは完全に真実ではありません。 他の一般的なJDBC操作と比較した
getConnection()呼び出しの数はかなり少ないです。
Connection 、
Statementなどのラッパーを最適化することにより、膨大な数のパフォーマンスの改善が達成されます。
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
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
35バイトコード命令の制限のすぐ下。 これは小さな方法であり、実際にはあまり負荷がかかりませんが、あなたはその考えを理解しました。 小さなメソッドでは、JITでコードを埋め込むことができるだけでなく、実際のマシン命令が少なくなるため、プロセッサのL1キャッシュに収まるコードの量が増えます。 ライブラリ内のこのような変更の数をすべてこれに掛けると、HickaryCPが本当に速い理由を理解できます。
マイクロ最適化
HikariCPには多くのマイクロ最適化があります。 それとは別に、彼らは確かに写真を作りません。 しかし、すべてを合わせると、全体的な生産性が大幅に向上します。 これらの最適化の一部は、数百万のコールのマイクロ秒の小数部です。
配列リスト
最も重要な最適化の1つは、開いている
Statementオブジェクトを追跡するために使用された
ConnectionProxyクラスの
ArrayList <Statement>コレクションを削除することでした。
Statementが終了したら、このコレクションから削除する必要があります。 また、接続が閉じている場合は、コレクションを調べて開いている
ステートメントをすべて閉じてから、コレクションをクリアする必要があります。 ご存知のように、
ArrayListは
get(index)の呼び出しごとにインデックスの範囲をチェックします。 ただし、正しいインデックスの選択を保証できるため、このチェックは不要です。 また、
remove(Object)メソッドの実装は、リストの最初から最後まで
渡されます。 同時に、JDBCで一般に受け入れられているパターンは、使用直後に
ステートメントを閉じるか、逆順(FILO)にすることです。 そのような場合、リストの最後から始まるパッセージが高速になります。 したがって、
ArrayList <Statement>を、範囲のチェックがなく、リストからのアイテムの削除が最後から始まる
FastStatementListに置き換えました。
遅いシングルトン
Connection 、
Statement 、
ResultSet 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
静的フィールド
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
ここでは、すぐに3つのポイントに注意を払う必要があります。
getstatic呼び出しはなくなりました。
invokevirtualは invokestaticに置き換えられ
ました 。これは、仮想マシンにより最適化されています。 最後に気づきにくいポイント-スタックのサイズが5要素から4に減少しました。 最適化の前に、
invokevirtualの場合、
ProxyFactoryオブジェクト自体へのリンクも
スタックに到達する必要があり
ます 。 これは、
getProxyPreparedStatement()が呼び出されたときにスタックからこのリンクを取得するための追加のポップ命令も意味します。 一般に、要約すると、静的フィールドへのアクセスを取り除き、スタック上の不要なプッシュおよびポップ操作を削除し、メソッド呼び出しをJIT最適化により適したものにしました。
終わり。
オリジナル
ダウンザラビットホールを完了してください。
更新:
コメントでは、「Slow Singleton」の記事が多くの議論を引き起こしました。
apanginは、これらのマイクロ最適化はすべて無意味であり、
利益をもたらさないと主張しています。 この
解説は、同じ値
invokeVirtualと
invokeStaticの単純なベンチマークを提供します。 そして、
ここにクラスメートの接続プールのベンチマーク
があります。これはおそらく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のパフォーマンスが向上しました!