web-dev-qa-db-ja.com

Arrays.fill()がHashMap.clear()で使用されなくなったのはなぜですか?

HashMap.clear()の実装で奇妙なことに気づきました。これが OpenJDK 7u4 の見た目です。

_public void clear() {
    modCount++;
    Arrays.fill(table, null);
    size = 0;
}
_

そして、これは OpenJDK 8u4 のように見えます:

_public void clear() {
    Node<K,V>[] tab;
    modCount++;
    if ((tab = table) != null && size > 0) {
        size = 0;
        for (int i = 0; i < tab.length; ++i)
            tab[i] = null;
    }
}
_

マップを空にするためにtableをnullにできるようになったので、ローカル変数での追加のチェックとキャッシュが必要であることを理解しています。しかし、なぜArrays.fill()はforループに置き換えられましたか?

変更は this commit で導入されたようです。残念ながら、プレーンループがArrays.fill()よりも優れている理由についての説明は見つかりませんでした。速いですか?またはより安全ですか?

70
Tagir Valeev

私は、コメントで提案された3つのもっと合理的なバージョンを要約しようとします。

@Holger says

これは、クラスJava.util.Arraysがこのメソッドの副作用としてロードされるのを避けるためだと思います。アプリケーションコードの場合、これは通常懸念事項ではありません。

これはテストが最も簡単なものです。そのようなプログラムをコンパイルしましょう:

_public class HashMapTest {
    public static void main(String[] args) {
        new Java.util.HashMap();
    }
}
_

_Java -verbose:class HashMapTest_で実行します。これにより、クラスロードイベントが発生すると出力されます。 JDK 1.8.0_60では、400を超えるクラスがロードされています。

_... 155 lines skipped ...
[Loaded Java.util.Set from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
[Loaded Java.util.AbstractSet from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
[Loaded Java.util.Collections$EmptySet from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
[Loaded Java.util.Collections$EmptyList from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
[Loaded Java.util.Collections$EmptyMap from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
[Loaded Java.util.Collections$UnmodifiableCollection from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
[Loaded Java.util.Collections$UnmodifiableList from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
[Loaded Java.util.Collections$UnmodifiableRandomAccessList from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
[Loaded Sun.reflect.Reflection from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
**[Loaded Java.util.HashMap from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
[Loaded Java.util.HashMap$Node from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
[Loaded Java.lang.Class$3 from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
[Loaded Java.lang.Class$ReflectionData from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
[Loaded Java.lang.Class$Atomic from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
[Loaded Sun.reflect.generics.repository.AbstractRepository from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
[Loaded Sun.reflect.generics.repository.GenericDeclRepository from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
[Loaded Sun.reflect.generics.repository.ClassRepository from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
[Loaded Java.lang.Class$AnnotationData from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
[Loaded Sun.reflect.annotation.AnnotationType from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
[Loaded Java.util.WeakHashMap from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
[Loaded Java.lang.ClassValue$ClassValueMap from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
[Loaded Java.lang.reflect.Modifier from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
[Loaded Sun.reflect.LangReflectAccess from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
[Loaded Java.lang.reflect.ReflectAccess from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
**[Loaded Java.util.Arrays from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
...
_

ご覧のとおり、HashMapはアプリケーションコードのかなり前にロードされ、ArraysHashMapの後の14クラスのみロードされます。 HashMapロードは、HashMap静的フィールドがあるため、_Sun.reflect.Reflection_初期化によってトリガーされます。 Arraysロードは、clear()メソッドに実際に_Arrays.fill_を含むWeakHashMapロードに​​よってトリガーされる可能性があります。 WeakHashMapロードは、WeakHashMapを拡張する_Java.lang.ClassValue$ClassValueMap_によってトリガーされます。 ClassValueMapは、すべての_Java.lang.Class_インスタンスに存在します。したがって、Arraysクラスがないと、JDKをまったく初期化できません。また、Arrays静的初期化子は非常に短く、アサーションメカニズムのみを初期化します。このメカニズムは、他の多くのクラスで使用されます(たとえば、非常に早くロードされる_Java.lang.Throwable_など)。 _Java.util.Arrays_では、他の静的な初期化手順は実行されません。したがって、@ Holgerのバージョンは間違っているようです。

ここでも非常に興味深いものが見つかりました。 WeakHashMap.clear()は引き続き_Arrays.fill_を使用します。そこに登場したのは興味深いですが、残念ながら 先史時代 になります(最初の公開OpenJDKリポジトリにすでに存在していました)。

次に、@ MarcoTopolnik says

確かに安全ではありませんが、fill呼び出しがインライン化されておらず、tabが短い場合、mightは高速になります。 HotSpotでは、ループと明示的なfill呼び出しの両方が、(ハッピーデイシナリオで)高速コンパイラ組み込み関数になります。

_Arrays.fill_が直接内在していないことは、私にとって実際には驚きでした( @ apangin によって生成される intrinsic list を参照)。このようなループは、明示的な組み込み処理を行わなくてもJVMによって認識およびベクトル化できるようです。そのため、非常に特殊な場合(たとえばMaxInlineLevelの制限に達した場合)に余分な呼び出しをインライン化できないことは事実です。一方、それは非常にまれな状況であり、単一の呼び出しのみであり、ループ内の呼び出しではなく、仮想/インターフェイス呼び出しではなく静的です。したがって、パフォーマンスの改善はごくわずかであり、特定のシナリオでのみ可能です。 JVM開発者が通常気にすることではありません。

また、C1 'クライアント'コンパイラ(ティア1-3)でも、インラインログ(_Arrays.fill_として、WeakHashMap.clear()などで呼び出される_-XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation -XX:+PrintInlining_をインライン化できることに注意してください。 )言う:

_36       3  Java.util.WeakHashMap::clear (50 bytes)
     !m        @ 4   Java.lang.ref.ReferenceQueue::poll (28 bytes)
                 @ 17   Java.lang.ref.ReferenceQueue::reallyPoll (66 bytes)   callee is too large
               @ 28   Java.util.Arrays::fill (21 bytes)
     !m        @ 40   Java.lang.ref.ReferenceQueue::poll (28 bytes)
                 @ 17   Java.lang.ref.ReferenceQueue::reallyPoll (66 bytes)   callee is too large
               @ 1   Java.util.AbstractMap::<init> (5 bytes)   inline (hot)
                 @ 1   Java.lang.Object::<init> (1 bytes)   inline (hot)
               @ 9   Java.lang.ref.ReferenceQueue::<init> (27 bytes)   inline (hot)
                 @ 1   Java.lang.Object::<init> (1 bytes)   inline (hot)
                 @ 10   Java.lang.ref.ReferenceQueue$Lock::<init> (5 bytes)   unloaded signature classes
               @ 62   Java.lang.Float::isNaN (12 bytes)   inline (hot)
               @ 112   Java.util.WeakHashMap::newTable (8 bytes)   inline (hot)
_

もちろん、スマートで強力なC2「サーバー」コンパイラーによって簡単にインライン化されます。したがって、ここで問題は発生しません。 @Marcoのバージョンも間違っているようです。

最後に、@ StuartMarksから comments がいくつかあります(JDKの開発者であり、公式の声です):

面白い。私の考えでは、これは間違いです。この変更セットのレビュースレッドは here であり、 以前のスレッド つまり ここに続く を参照しています。その以前のスレッドの最初のメッセージは、Doug LeaのCVSリポジトリにあるHashMap.Javaのプロトタイプを指しています。これがどこから来たのか分かりません。 OpenJDKの歴史にあるものとは一致しないようです。

...いずれにせよ、それは古いスナップショットであったかもしれません。 forループは長年にわたってclear()メソッドに含まれていました。 Arrays.fill()呼び出しは this changeset によって導入されたため、数か月間だけツリーに存在していました。また、 this changeset によって導入されたInteger.highestOneBit()に基づく2のべき乗計算も同時に消失したことに注意してください。ただし、これは確認中に却下されました。うーん。

実際、HashMap.clear()は長年ループを含んでおり、2013年4月10日に_Arrays.fill_で replaced でした。 commit が導入されました。議論されたコミットは、実際にはHashMap内部を大幅に書き直して JDK-802346 の問題を修正したものです。重複するハッシュコードを持つキーでHashMapをポイズニングする可能性についての長い話でしたが、HashMapの検索速度が線形に低下し、DoS攻撃に対して脆弱になりました。これを解決する試みは、String hashCodeのランダム化を含むJDK-7で実行されました。 HashMapの実装は、以前のコミットから分岐し、独立して開発された後、マスターブランチにマージされ、その間に導入されたいくつかの変更を上書きしているようです。

差分を実行するこの仮説を支持するかもしれません。 バージョン _Arrays.fill_が削除された場所(2013-09-04)を取得し、それを 前のバージョン (2013-07-30)と比較します。 _diff -U0_出力には4341行があります。 _Arrays.fill_が追加されたとき(2013-04-01)、 version の前のdiffと比較しましょう。現在、_diff -U0_には2680行のみが含まれています。したがって、新しいバージョンは実際には、直接の親よりも古いバージョンに似ています。

結論

結論として、スチュアートマークスに同意します。 _Arrays.fill_を削除する具体的な理由はありませんでした。これは、中間の変更が誤って上書きされたためです。 _Arrays.fill_の使用は、JDKコードとユーザーアプリケーションの両方で問題なく使用でき、たとえばWeakHashMapで使用されます。 Arraysクラスは、JDKの初期化のかなり早い段階でロードされ、非常に単純な静的イニシャライザーと_Arrays.fill_メソッドをクライアントコンパイラーでも簡単にインライン化できるため、パフォーマンスの低下に注意する必要はありません。

29
Tagir Valeev

それはmuchより速いからです!

私は、2つのメソッドの削減バージョンでいくつかの徹底的なベンチマークテストを実行しました。

void jdk7clear() {
    Arrays.fill(table, null);
}

void jdk8clear() {
    Object[] tab;
    if ((tab = table) != null) {
        for (int i = 0; i < tab.length; ++i)
            tab[i] = null;
    }
}

ランダムな値を含むさまざまなサイズの配列を操作します。 (典型的な)結果は次のとおりです。

Map size |  JDK 7 (sd)|  JDK 8 (sd)| JDK 8 vs 7
       16|   2267 (36)|   1521 (22)| 67%
       64|   3781 (63)|   1434 ( 8)| 38%
      256|   3092 (72)|   1620 (24)| 52%
     1024|   4009 (38)|   2182 (19)| 54%
     4096|   8622 (11)|   4732 (26)| 55%
    16384|  27478 ( 7)|  12186 ( 8)| 44%
    65536| 104587 ( 9)|  46158 ( 6)| 44%
   262144| 445302 ( 7)| 183970 ( 8)| 41%

そして、nullで満たされた配列を操作した場合の結果は次のとおりです(したがって、ガベージコレクションの問題は根絶されます)。

Map size |  JDK 7 (sd)|  JDK 8 (sd)| JDK 8 vs 7
       16|     75 (15)|     65 (10)|  87%
       64|    116 (34)|     90 (15)|  78%
      256|    246 (36)|    191 (20)|  78%
     1024|    751 (40)|    562 (20)|  75%
     4096|   2857 (44)|   2105 (21)|  74%
    16384|  13086 (51)|   8837 (19)|  68%
    65536|  52940 (53)|  36080 (16)|  68%
   262144| 225727 (48)| 155981 (12)|  69%

数値はナノ秒、(sd)は、結果の割合として表される1標準偏差(fyi、「正規分布」母集団のSDは68です)、vsは、JDK 7に対するJDK 8のタイミングです。

興味深いのは、それが大幅に高速であるだけでなく、偏差がわずかに狭いことも意味します。つまり、JDK 8の実装では、より多くのconsistentパフォーマンスが得られます。

テストは、jdk 1.8.0_45で、ランダムなIntegerオブジェクトが配置された配列に対して、何百万回も実行されました。外れた数値を削除するために、結果の各セットで、最速および最遅の3%のタイミングが破棄されました。メソッドの各呼び出しを実行する直前に、ガベージコレクションが要求され、スレッドが生成されてスリープしました。 JVMのウォームアップは作業の最初の20%で行われ、それらの結果は破棄されました。

3
Bohemian

私にとって、その理由は、コードの明瞭さの観点からごくわずかなコストでパフォーマンスが向上する可能性が高いことです。

fillメソッドの実装は簡単で、各配列要素をnullに設定する単純なforループであることに注意してください。そのため、呼び出しを実際の実装で置き換えても、呼び出し元メソッドの明確性/簡潔性が大幅に低下することはありません。

関連するすべてを考慮すると、潜在的なパフォーマンスの利点はそれほど重要ではありません。

  1. JVMがArraysクラスを解決する必要はなく、必要に応じてそれをロードして初期化します。これは、JVMがいくつかのステップを実行する重要なプロセスです。まず、クラスローダーをチェックして、クラスが既にロードされているかどうかを確認します。これは、メソッドが呼び出されるたびに発生します。もちろん、ここには最適化が含まれますが、それでも多少の努力が必要です。クラスがロードされていない場合、JVMはロード、バイトコードの検証、その他の必要な依存関係の解決、およびクラスの静的初期化(最終的には任意のコストがかかる可能性があります)のロードという高価なプロセスを実行する必要があります。 HashMapがそのようなコアクラスであり、Arraysがこのような巨大なクラス(3600行以上)である場合、これらのコストを回避すると、大幅な節約になります。

  2. Arrays.fill(...)メソッド呼び出しがないため、JVMはメソッドを呼び出し側の本体にインライン化するかどうか/いつインライン化するかを決定する必要はありません。 HashMap#clear()は頻繁に呼び出される傾向があるため、JVMは最終的にインライン化を実行します。これには、clearメソッドのJIT再コンパイルが必要です。メソッド呼び出しがない場合、clearは常に最高速度で実行されます(最初にJITされると)。

Arraysでメソッドを呼び出さなくなるもう1つの利点は、1つの依存関係が削除されるため、Java.utilパッケージ内の依存関係グラフが簡素化されることです。

1
Rogério

ここで暗闇で撮影します...

私のguessは、 Specialization (プリミティブ型に対するジェネリック)の基盤を準備するために変更された可能性があることです。 Maybe(そして、私はmaybeを主張します)、この変更は、Java = 10は、JDKの一部である専門化の場合に簡単です。

Specializationドキュメントの状態言語制限セクションを見ると、次のように表示されます。

型変数は参照型と同様に値をとることができるため、そのような型変数に関連する型チェック規則(以下、「avars」)。たとえば、Avar Tの場合:

  • nullを型がTの変数に変換できません
  • Tをnullと比較できません
  • Tをオブジェクトに変換できません
  • T []をObject []に変換できません
  • ...

(エンファシスは私のものです)。

そして、Specializer変換セクションでは、次のように述べています:

任意の汎用クラスを特化する場合、スペシャライザーは多くの変換を実行しますが、最もローカライズされたものもありますが、一部にはクラスまたはメソッドのグローバルビューが必要です。

  • ...
  • すべてのメソッドのシグネチャに対して型変数の置換と名前のマングリングが実行されます
  • ...

後で、ドキュメントの終わり近くで、さらなる調査セクションで、それは言います:

私たちの実験は、この方法での専門化が実用的であることを証明していますが、さらに多くの調査が必要です。具体的には、あらゆるコレクションのコアJDKライブラリ、特にコレクションとストリームを対象とした、多くのターゲットを絞った実験を実行する必要があります。


さて、変更について...

Arrays.fill(Object[] array, Object value)メソッドが特殊化される場合、その署名はArrays.fill(T[] array, T value)に変更する必要があります。ただし、このケースは(既に言及した)Language restrictionセクションに明確にリストされています(強調された項目に違反します)。そのため、多分誰かが、特にvaluenullの場合、HashMap.clear()メソッドから使​​用しない方が良いと判断しました。

2つのバージョンのループの機能に実際の違いはありません。 Arrays.fillはまったく同じことを行います。

したがって、それを使用するかどうかの選択は、必ずしも間違いとは限りません。この種のマイクロ管理に関しては、開発者が決定する必要があります。

各アプローチには2つの個別の懸念事項があります。

  • Arrays.fillを使用すると、コードが冗長にならず読みやすくなります。
  • (バージョン8のように)HashMapコードで直接ループすることは、実際にはパフォーマンスの方が優れたオプションです。 Arraysクラスを挿入するオーバーヘッドは無視できますが、パフォーマンス向上のすべてのビットが大きな影響を与えるHashMapのようなものになると、小さくなる可能性があります(最小のフットプリントを想像してください本格的なwebappのHashMapの)。 Arraysクラスがこの1つのループにのみ使用されたという事実を考慮してください。変更は十分に小さいため、clearメソッドが読みにくくなることはありません。

正確な理由は、実際にこれを行った開発者に尋ねることなく見つけることはできませんが、それは間違いまたは小さな機能強化のどちらかだと思います。より良いオプション。

私の意見では、たとえ偶然であったとしても、それは機能強化と見なすことができます。

0
Laurentiu L.