Java 17 で String.format メソッドが数倍 速くなったので確認する

投稿: 2021年11月01日
タグ: 
  Developer(Java等)  技術メモ  ニュースネタ

Java 17 の String.format メソッドが 3倍速くなった、という こちらの記事open_in_new を見て、実際にどうなのか手元でも確認してみた。

先に結論から。Java 11 と Java 17 双方で String.format を実行したところ、Java 17 の処理時間が Java 11 と比較して約 1/4 になった。 Java 17 の方が 4倍近く速くなっている。 ただ、どんなパターンでも必ず4倍速くなるわけではない (詳細は後述)。

Java 11 と Java 17 での String.format 実行時間比較
バージョン実装方法平均値中央値
Java 11String.format10781062
Java 17String.format274268
単位: ミリ秒
  • 実際試したパターンでは、Java 11 ⇒ Java 17 で String.format は約4倍 速くなっている
  • それでも StringBuilder で実装するよりは大幅に遅い(のでループの中で多数実行する際は引き続き注意)
  • Java 11 (11.0.2) と Java 17 (17ga 17+35) で String.format の処理時間を比較

    計測のために実行したのは以下の簡単なコードで、String.format を 100万回 ぶん回して時間を計測している。 1回の実行あたりではナノ秒レベルで完了するため、100万回実行している。

    Java
    void formatString() { long start = System.nanoTime(); for (int i = 0; i < 1_000_000; i++) { String.format("メッセージ1=[%s] メッセージ2=[%s] メッセージ3=[%s]", "Message 1", "Message 2", "Message 3"); } long end = System.nanoTime(); System.out.printf("formatString: %sms%n", (end - start) / 1000 / 1000); }

    それぞれ Java 11 と Java 17 で上記コードを 100回ずつ試行して、(以下のように出力された) 結果を確認した。 尚、Java のディストリビューションは OpenJDK を利用しており、Java 11 は 11.0.2、Java 17 は 17+35 のバージョンを利用した。

    formatString: 268ms
    formatString: 284ms
    formatString: 277ms
    formatString: 269ms
    formatString: 260ms

    Java 17 の String.format が Java 11 と比較して 4倍近く速い結果となった。 ただ、StringBuilder と比較するとそれでも遅い

    100回の計測結果の平均値と中央値は以下になった。めちゃくちゃ速くなっている。

    String.format 実行時間比較
    
    Java 11 (11.0.2) 
    平均値:1078ミリ秒、中央値:1062ミリ秒
    
    Java 17 (ga 17+35)
    平均値: 274ミリ秒、中央値: 268ミリ秒

    StringBuilder での連結との比較も同様に実行してみた。実行したコードは以下の通りで、String.format メソッドの替わりに StringBuilder で文字列を連結している。 String.format と同様、100万回の実行時間を計測し、それを 100回実行した。

    Java
    void appendString() { String msg1 = "Message 1"; String msg2 = "Message 2"; String msg3 = "Message 3"; long start = System.nanoTime(); StringBuilder sb = new StringBuilder(); for (int i = 0; i < 1_000_000; i++) { sb.append("メッセージ1=[").append(msg1) .append("] メッセージ2=[").append(msg2) .append("] メッセージ3=[").append(msg3).append("]"); } long end = System.nanoTime(); System.out.printf("appendString: %sms%n", (end - start) / 1000 / 1000); }

    結果は以下の通りとなった。Java 17 の方が 1割ほど速い結果となったが、これだけでは有意な差かは分からない。 とりあえず、Java 17 の String.format と比較すると圧倒的に速い。

    StringBuilder.append 実行時間比較
    
    Java 11 (11.0.2) 
    平均値: 83ミリ秒、中央値: 85ミリ秒
    
    Java 17 (ga 17+35)
    平均値: 74ミリ秒、中央値: 72ミリ秒

    まとめると以下の通りとなる。

    バージョン実装方法平均値中央値
    Java 11String.format10781062
    Java 17String.format274268
    Java 11StringBuilder.append8385
    Java 17StringBuilder.append7472
    単位: ミリ秒

    Java 17 でも圧倒的に StringBuilder の方が処理時間は短いので、 『大量データを処理するバッチジョブとかで繰り返し呼ばれる可能性のあるメソッド』等、 このレベルの差が問題になる箇所では引き続き StringBuilder を使った方がよいだろう。

    なぜ Java 17 で String.format のパフォーマンスが改善したか

    String.format のパフォーマンス改善は以下 Issue で対応されている。

    8263038: Optimize String.format for simple specifiers #2830
    https://github.com/openjdk/jdk/pull/2830open_in_new

    修正が入る前(Java 11) の java.util.Formatter の コードopen_in_new を見ると、フォーマット対象文字列の長さ分 毎回 正規表現のチェック(java.util.regex.Matcher#find) を行っている。 これは時間がかかりそうだ。

    Java 11
    private List<FormatString> parse(String s) { ArrayList<FormatString> al = new ArrayList<>(); Matcher m = fsPattern.matcher(s); for (int i = 0, len = s.length(); i < len; ) { if (m.find(i)) {

    String.format が使われるのは どうせ『format("foo: %s", str)』 といったパターンなのだから 毎回 正規表現でチェックしたりなどの処理を省けるよね、という修正がされている。

    そのため、殆どのパターンで String.format は速くなるが、Formatter で想定されていないパターンや複雑なパターンになると処理時間が長くなる。 例えば、以下のパターンを試したら、平均処理時間(100万回実行時の処理時間) が 730ミリ秒ほどになった (それでも Java 11 よりは速い)。

    Java
    String.format("メッセージ98=[%98s]", "98個目のパラメータ", "メッセージ99=[%99s]", "99個目のパラメータ", "メッセージ100=[%100s]", "100個目のパラメータ");

    上記は formatメソッドに 100個パラメータを渡した場合、という例だが、こんな使い方になる場合は そもそもの実装を考えた方がよいと思うので、 基本的には Java 17 で String.format のパフォーマンスが向上する、と考えてよいと思われる。

    String.format が遅い、というのはなんとなく把握していたが、実際に確認してみたことはなかったので、良い機会になった。
    正直、ミリ秒以下のレベルを争うコードでなければどっちでも良いと思う。 それでも、こういうプリミティブな更新は素晴らしいし、こういう修正にリソースを使う言語はプログラミング言語としての信頼感が上がる。