原文はこちら。
The original article was written by Per-Ake Minborg ().
https://inside.java/2025/05/01/strings-just-got-faster/
JDK 25では、Stringクラスのパフォーマンスを改善し、String::hashCode関数がほぼ定数畳み込み可能(constant foldable)になりました。例えば、静的で変更不可能なMapのキーとしてStringを使用する場合、大幅なパフォーマンス向上が期待できます。
8354300: Mark String.hash field @Stable #24625
https://github.com/openjdk/jdk/pull/24625
Constant folding
https://en.wikipedia.org/wiki/Constant_folding
https://ja.wikipedia.org/wiki/定数畳み込み
Example
以下は、ネイティブ呼び出しのImmutableなMapをメンテナンスする比較的高度な例です。このMapのキーはメソッド呼び出しの名前であり、値は関連するシステム呼び出しを実行するために使用できるMethodHandleです。
// Set up an immutable Map of system calls
static final Map<String, MethodHandle> SYSTEM_CALLS = Map.of(
“malloc”, linker.downcallHandle(mallocSymbol,…),
“free”, linker.downcallHandle(freeSymbol…),
...);
…
// Allocate a memory region of 16 bytes
long address = SYSTEM_CALLS.get(“malloc”).invokeExact(16L);
…
// Free the memory region
SYSTEM_CALLS.get(“free”).invokeExact(address);
linker.downcallHandle(…) というメソッドは、シンボルと追加のパラメータを受け取り、JDK 22で導入されたForeign Function & Memory APIを使い、ネイティブ呼び出しをJava MethodHandleにバインドします。
JEP 454: Foreign Function & Memory API
https://openjdk.org/jeps/454
これは比較的遅いプロセスであり、バイトコードの展開を伴います。ただし、Mapに格納されると、Stringクラスの新しいパフォーマンス改善により、キーの検索と値の両方の定数化が可能になり、パフォーマンスが8倍以上向上します。
--- JDK 24 ---
Benchmark Mode Cnt Score Error Units
StringHashCodeStatic.nonZero avgt 15 4.632 ± 0.042 ns/op
--- JDK 25 ---
Benchmark Mode Cnt Score Error Units
StringHashCodeStatic.nonZero avgt 15 0.571 ± 0.012 ns/op
【注意】上のベンチマークでは、malloc() MethodHandleではなく、intのアイデンティティ関数を使用しています。結局のところ、malloc()のパフォーマンスをテストしているのではなく、実際の文字列検索とメソッドハンドルのパフォーマンスをテストしているからです。
この改善は、キーとして文字列を使用し、値(任意の型 V)を定数文字列で検索するImmutableなMap<String, V>に適用されます。
How Does It Work?
Stringが最初に作成されたとき、そのハッシュコードは未知です。String::hashCodeが呼び出されて初めて実際のハッシュコードが計算され、プライベートフィールドString.hashに格納されます。この変換は奇妙に思えるかもしれません。StringがImmutableなら、なぜ状態が変更されるのでしょうか?答えは、この変更は外部から観察できないためです。Stringは、内部のString.hashキャッシュフィールドを使用するか否かにかかわらず、機能的には同じように動作します。唯一の違いは、以降の呼び出しが高速化される点です。
String::hashCodeの動作が分かったところで、パフォーマンス変更の内容(たった1 行のコード)を明らかにします。内部フィールドString.hashに JDK internalの@Stableアノテーションが付けられました。これだけです!
@Stableにより、仮想マシンは以下のように指示されます。
- そのフィールドを一度だけ読み取る
- デフォルト値(0)でなくなった場合、そのフィールドが二度と変更されないことを信頼する
それゆえ、String::hashcode操作を定数展開し、既知のhashで関数呼び出しの置換が可能です。実際、ImmutableなMap内のフィールドとMethodHandle内部の値も同様の信頼性で扱われます。これはつまり、仮想マシンは操作の全体チェーンを定数化できる、ということです。
- Stringの”malloc”のハッシュコードを計算(常に-1081483544)
- Immutableの
Mapを検査(つまり、mallocハッシュコードに対して常に同じ内部配列インデックスを計算) - 関連するMethodHandleを取得 (常に前述の計算済みインデックスに存在)
- 実際のネイティブ呼び出しを解決(常にネイティブの
malloc()呼び出し)
結果として、ネイティブのmalloc()メソッド呼び出しが直接実行可能になることが、大幅なパフォーマンス向上の理由です。換言すれば、操作のチェーンが完全にショートカットされます。
What Are the Ifs and Buts?
新しい改善点は、残念ながら次のケースをカバーしていません。
Stringのハッシュコードがゼロの場合、定数折り畳みが機能しない
上述の通り、定数折り畳みはデフォルト値以外の値(つまり、intフィールドのゼロ以外の値)に対してのみ発生します。ただし、この小さな問題は近い将来に修正できる見込みです。約40億の異なる文字列のうち、ハッシュコードが0であるものは1つだけだと考えるかもしれません。平均的にはその通りかもしれません。しかし、最も一般的な文字列(空文字列 ”” )のハッシュ値は0です。一方、1文字から6文字までの文字列(すべての文字が空白スペースからZまで)でハッシュコードが0の文字列は存在しません。
A Final Note
@Stableアノテーションはinternal JDKコードにのみ適用可能であるため、Javaアプリケーションで直接使用できません。ただし、ユーザーコードが@Stableフィールドから間接的に恩恵を受けることができる新しいJEP(JEP 502: Stable Values (Preview))の開発を進めています。
JEP 502: Stable Values (Preview)
https://openjdk.org/jeps/502
What’s the Next Step?
JDK 25(早期アクセスビルド)は既にダウンロード可能で、現在のアプリケーションにこのパフォーマンス向上がどの程度役立つかを確認できます。
JDK 25 Early Access Builds
https://jdk.java.net/25/