読者です 読者をやめる 読者になる 読者になる

Java 1.7.0_06でStringが仕様変更されたのでベンチマークをしてみた

Java 1.7.0_06でJavaの基本型であるStringが仕様変更されたので、それがパフォーマンスにどの程度影響するのか簡単なベンチマークをして、比較をしてみました。

何が変わったのか

String型の仕様が変更されたのを知ったのは、下記の記事がきっかけで、変更内容についてもわかりやすく説明されています。
実はStringはメモリリークの原因だった(※1.7.0_06未満) - R42日記

簡単に説明すると、String型が持つインスタンスフィールドのうち、

  • offset (文字列の内容が入っているchar型配列を参照する最初の位置)
  • count (文字列の長さ)

の2つが削除されました。
理由は以下です。(上の記事から引用)

substringは、新しい文字列インスタンスを作成する際に、元の文字列のchar[ ]を参照する。
変更前の実装は、メモリの節約に主眼をおいて設計されていた一方で、元のchar[ ]を参照し続けることになる。付け加えると、変更前のsubstringの実装は定数時間( O(1) )で動作する一方で、新しい実装は線形時間( O(n) )で動作する。
変更前の実装では、元は大きな文字列だったものからsubstringで小さな文字列を切り出した場合に、元のchar[ ]への参照が生き続ける。つまり、もはや不要なはずの領域がガベージコレクトされずに残り続ける。

メモリの節約をするためだった実装が、実はメモリリークの原因になっていたということで、Java 1.7.0_06で修正が入ったということです。

ソースコードを見てみる

Java 1.7.0_05とJava 1.7.0_06のjava.lang.Stringの差分を取ってみました。
substring(int beginIndex, int endIndex)に関する部分だけ抜粋します。

--- jdk1.7.0_05/src/java/lang/String.java
+++ jdk1.7.0_06/src/java/lang/String.java
@@ -1951,14 +1903,15 @@
         if (beginIndex < 0) {
             throw new StringIndexOutOfBoundsException(beginIndex);
         }
-        if (endIndex > count) {
+        if (endIndex > value.length) {
             throw new StringIndexOutOfBoundsException(endIndex);
         }
-        if (beginIndex > endIndex) {
-            throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
+        int subLen = endIndex - beginIndex;
+        if (subLen < 0) {
+            throw new StringIndexOutOfBoundsException(subLen);
         }
-        return ((beginIndex == 0) && (endIndex == count)) ? this :
-            new String(offset + beginIndex, endIndex - beginIndex, value);
+        return ((beginIndex == 0) && (endIndex == value.length)) ? this
+                : new String(value, beginIndex, subLen);
     }

countとoffsetが削除されたためそれに変わる処理に置き換えられているとともに、最後の5行(returnではじまる文)に注目してもらいたいのですが、1.7.0_05では、

new String(offset + beginIndex, endIndex - beginIndex, value);

となっているのに対し、1.7.0_06では、

new String(value, beginIndex, subLen);

となっています。
前者のString(int, int, char)メソッドは、元のcharへの参照が生き続ける実装で、後者のString(char, int, int)メソッドはcharの中から必要な物だけをコピーする実装になっています。
ということは、1.7.0_06より配列の内容をコピーする処理が増えたわけなので、処理時間にも差が出るのでは…?ということで、簡単に調べてみました。

ベンチマークしてみた

処理時間を比べるコードは以下です。適当な26文字の文字列をsubstringで半分にして、それをまた結合して…という処理を1億回繰り返します。
実行環境は、さくらVPSの1Gプランです。

public class StringSplitTest {

    public static void main(String[] args) {
        int TIME = 100000000;
        long start = System.currentTimeMillis();
        String str = "qwertyuiopasdfghjklzxcvbnm";
        for (int i = 0; i < TIME; i++) {
            String a = str.substring(0, 13);
            String b = str.substring(13, 26);
            str = b + a;
        }
        long end = System.currentTimeMillis();
        System.out.println((end - start) + "ms");
    }

}

実行してみます。結果は、
Java 1.7.0_05

$ jdk1.7.0_05/bin/java StringSplitTest
5654ms

Java 1.7.0_06

$ jdk1.7.0_06/bin/java StringSplitTest
8237ms

となりました。
8237 / 5654 = 1.4568...
ですので、約1.5倍遅くなったということになります。