地平線に行く

2012-08-27

Java7 Update6 で String クラスがさらにリファクタリングされていました

| 22:59 |

2012年8月14日に登場した Java SE 7 Update6 で、またしても String クラスがリファクタリングされていました!

そこで、そこがどういう風に変わったのかを詳しく調べてみました。


フィールド変数 count と offset が削除されました

Stringクラスにあった4つのフィールド変数のうち、count と offset が削除されました。

     /** The value is used for character storage. */
     private final char value[];
 
-    /** The offset is the first index of the storage that is used. */
-    private final int offset;
-
-    /** The count is the number of characters in the String. */
-    private final int count;
-
    /** Cache the hash code for the string */
    private int hash; // Default to 0

この変更が行われた経緯ですが…。


そもそもこの2つの変数は、String#substring メソッドで部分文字列を生成する際に、フィールド変数 char[] value (文字列を格納しているバッファ)を共有するために使われていました。*1

    String str = "あいうえお!";
    String substr = str.substring(2, 5);    // 戻り値:"うえお"

    // Java7 Update5 だと、以下は「TRUE」
    System.out.println(str.value == substr.value);

上記の例だと str の中身は以下のようになっています。

    char[] value = {'あ', 'い', 'う', 'え', 'お', '!'};
    int offset = 0;
    int count = 7;

一方で、substr の中身は以下のようになっています。

    char[] value = str.value;
    int offset = 2;
    int count = 3;

このように文字バッファ(value)を共有しつつ、その配列の開始位置(offset)と終了位置(offset + count)を変えることによって違う文字列にしています。

これによって、substring メソッドの高速化と、全体的なメモリ使用量の削減を狙っていたようです。


しかし、実際は思ったよりも効果がなかったようです。

この変更に対するレビュー依頼に、以下のような記載がありました。

Shared character buffers were an important optimization for old benchmarks but with current real world code and benchmarks it's actually better to not share backing buffers. Shared char array backing buffers only "win" with very heavy use of String.substring. The negatively impacted situations can include parsers and compilers however current testing shows that overall this change is beneficial.


文字バッファの共有は、古いベンチマークにとっては重要な最適化でしたが、現在の現実の世界のコードやベンチマークからすれば、文字バッファの共有はしないほうが実際にはよいです。char配列バッファの共有が有効なのは、String.substring を非常に多用した場合のみです。パーサやコンパイラー含めて悪い影響があるかもしれませんが、現在のテストはこの変更全体に利益があることを示しています。

Request for Review : CR#6924259: Remove String.count/String.offset

このような経緯で、文字バッファの共有をやめると同時に、offset と count は削除されたようです。

最終的に、Stringクラス内で offset 変数が使われていた箇所は 0 に、count 変数が使われていた箇所は value.length に置き換えられていました。また、全体的に処理もだいぶシンプルになっていました。


ちなみに、この変更は String クラスだけにとどまらず、JavaVM 側でも修正が行われたようです。

jdk7u/jdk7u/hotspot: changeset 3384:3facbb14e873

そのせいなのか、リリースノートにはカテゴリー「 hotspot:compiler2 」で記載されていました。


コンストラクタ最適化

StringBuilder, StringBuffer を引数にとるコンストラクタが、最適化されていました。

// Java7 Update5
    public String(StringBuilder builder) {
        String result = builder.toString();
        this.value = result.value;
        this.count = result.count;
        this.offset = result.offset;
    }
// Java7 Update6
    public String(StringBuilder builder) {
        this.value = Arrays.copyOf(builder.getValue(), builder.length());
    }

Update5 だと、StringBuilder#toString() 内部で文字列を生成してから中身をコピーしていましたが、Update6だと StringBuilder から直接配列をコピーするようになっていました。

これにより、ほんのわずかですが余分なオブジェクトを生成しなくて済むようになっていました。


配列コピーの最適化

char配列を生成して System.arraycopy で全体をコピーしていた箇所が、Arrays.copyOf を使用するように変更になっていました。

// Java7 Update5
    public String concat(String str) {
        (中略)
        // ↓ 配列を生成し、getChars 内部で System.arraycopy を呼んでコピー
        char buf[] = new char[count + otherLen];
        getChars(0, count, buf, 0);
        str.getChars(0, otherLen, buf, count);
        return new String(0, count + otherLen, buf);
    }
// Java7 Update6
    public String concat(String str) {
        (中略)
        // ↓ 配列生成を Arrays.copyOf に委譲
        int len = value.length;
        char buf[] = Arrays.copyOf(value, len + otherLen);    
        str.getChars(buf, len);
        return new String(buf, true);
    }

この変更理由は、VM最適化が効くから、だそうです。


たまたま、レビューを依頼した段階では逆方向の修正(Arrays.copyOf を使っていた箇所を、new char[len] と System.arraycopy メソッドの呼び出しに変更)していて、それをレビュアー(Rémi Forax氏)が以下のように指摘していました。

Also, the changeset remplace several usages of

  this.value = Arrays.copyOf(value, size);

by

  this.value = new char[len];

  System.arraycopy(value, 0, this.value, 0, len);

I think it's not a good idea, Arrays.copyOf is recognized as an intrinsics and avoid to initialize the array with zeroes. I know that c2 is able to transform new + arraycopy to copyOf but I don't think c1 does that.


また、チェンジセットの中でいくつかの個所が

  this.value = Arrays.copyOf(value, size);

以下のように置き換えられていました

  this.value = new char[len];

  System.arraycopy(value, 0, this.value, 0, len);

これは良いアイデアではないと思います。Arrays.copyOf は組み込み関数として認識され、ゼロで配列初期化するのを回避されています。私は c2 (訳注:ServerVMのこと) が new + arraycopy を copyOf に変換しているのを知っているが、c1 (訳注:ClientVMのこと) はそれをしないと思う。

Request for Review : CR#6924259: Remove String.count/String.offset

たぶん、intrinsics(組み込み関数)というのは JavaVM の library_call.cpp 内にある Librarycallkit::inline_array_copyof(bool is_copyOfRange) のことを指しているのかなと思います*2

その中で、不要な配列初期化を行わないようにしているので、Arrays.copyOf のままのほうがいいという指摘のようです。


この指摘を受けて、該当箇所を戻しつつ、ついでに全体を見直した際に上記の個所も修正されたようです。

細かい修正のようですが、基本的なクラスなのでこのような細かい修正でも効果があるのではないかと思います。


感想

一年ぐらい前に Java7 で String クラスがリファクタリングされていました を書いたときも、こんな改良の余地があるんだと驚いたのですが、今回はさらにそれを上回る驚きでした。

それと、今回の2番目と3番目は、レビュー指摘を受けての修正でした。こういうのを見てると、改めてコードレビューは大事なんだなと思いました。*3


あと、リファクタリングではなく機能追加だったんで書かなかったんですが「 Alternative Hashing 」についても、ちょっと調べたいなと思います。

*1:以下は模擬コードです。実際は、String#value が private 変数なので、最終行でコンパイルエラーになります。

*2:これについては、あんまり自信ないです…。

*3:よく言われる「品質が上がる」というのだけでなく、レビューを通して周りの人にも知識が伝わっていくという点。

リンク元