Java 9でも、ArrayListは引き続き改善できます(改善する必要があります)

ほとんどのジャビストは、 java.util.ArrayListがJavaの世界で最も使用されているコレクションであることに同意すると思います。 ほとんどの場合、その機能は日常の作業に十分であるため、バージョン1.2で登場し、すぐに「デフォルトのコレクション」になりました。 このクラスをできるだけ生産的にするために、このクラスには多くの変更が加えられています(たとえば、 JDK 8リポジトリの変更履歴を参照)。 この記事では、 ArrayListようなポンプされたコンポーネントでさえ、改善の余地があることを示します。


リストの一部を配列に変換する必要があるとします。 これを行うには、メソッドを説明します。


 public T[] toSubArray(ArrayList<T> list, int from, int to) { return list .subList(from, to) .toArray(new T[0]); } 

元のリストの配列への変換と比較して、そのパフォーマンスを評価してみましょう。


 @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) @Fork(jvmArgsAppend = {"-XX:+UseParallelGC", "-Xms1g", "-Xmx1g"}) public class SubListToArrayBenchmark { /** * baseline */ @Benchmark public Integer[] list(Data data) { return data.list.toArray(new Integer[0]); } @Benchmark public Integer[] subList(Data data) { return data.list.subList(0, data.size).toArray(new Integer[0]); } @State(Scope.Thread) public static class Data { ArrayList<Integer> list; @Param({"0", "10", "100", "1000"}) int size; @Setup public void setup() { list = IntStream .range(0, size) .boxed() .collect(toCollection(ArrayList::new)); } } } 

測定が完了すると、 subList()メソッドのパフォーマンスはベースラインのパフォーマンスよりも著しく劣ることがわかります。


ベンチマークサイズ得点エラー単位
リスト07.20.1ns / op
subList012.80.2ns / op
リスト1034.63.9ns / op
subList1044.71,0ns / op
リスト100141.92.2ns / op
subList100252.14.9ns / op
リスト10001201.621.0ns / op
subList10002310.453.0ns / op

どちらの場合も同じ量のデータが移動するという事実を考えると、この大きな違いは驚くべきことです。


キーはArrayListクラスにあります。


 public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { //... public Object[] toArray() { return Arrays.copyOf(elementData, size); } public <T> T[] toArray(T[] a) { if (a.length < size) // Make a new array of a's runtime type, but my contents: return (T[]) Arrays.copyOf(elementData, size, a.getClass()); System.arraycopy(elementData, 0, a, 0, size); if (a.length > size) a[size] = null; return a; } //... } 

どちらのメソッドも、 Arrays.copyOf()およびSystem.arraycopy()を使用してデータを移動し、配列に直接アクセスします。 中を見てみましょう:


 public class Arrays { //... @HotSpotIntrinsicCandidate // since Java 9 public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) { @SuppressWarnings("unchecked") T[] copy = ((Object)newType == (Object)Object[].class) ? (T[]) new Object[newLength] : (T[]) Array.newInstance(newType.getComponentType(), newLength); System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength)); return copy; } //... } 

 public final class System { //... @HotSpotIntrinsicCandidate // since Java 9 public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length); //... } 

指定されたメソッドは@HotSpotIntrinsicCandidateとしてマークされ、VMを許可します それらのための高性能マシンコードを作成する 実装を高性能マシンコードに置き換えて、最高のパフォーマンスを実現します。


さて、 subList()メソッドを見てみましょう。


 public List<E> subList(int fromIndex, int toIndex) { subListRangeCheck(fromIndex, toIndex, size); return new SubList<>(this, fromIndex, toIndex); } 

ご覧のとおり、 ArrayListはこのメソッドの独自の実装があり、さらに重要なこととして、リスト部分のプレゼンテーションの独自の実装があります。


 private static class SubList<E> extends AbstractList<E> implements RandomAccess { private final ArrayList<E> root; private final SubList<E> parent; private final int offset; private int size; //... } 

主なもの: SubList RandomAccessとしてマークされ、 rootフィールドを介して配列に直接アクセスできますが、 toArray()およびtoArray(T[])メソッドの独自の実装はありません 。 もしそうなら、 AbstractCollectionクラスの継承されたメソッドが使用されます:


 public Object[] toArray() { // Estimate size of array; be prepared to see more or fewer elements Object[] r = new Object[size()]; Iterator<E> it = iterator(); for (int i = 0; i < r.length; i++) { if (! it.hasNext()) // fewer elements than expected return Arrays.copyOf(r, i); r[i] = it.next(); } return it.hasNext() ? finishToArray(r, it) : r; } public <T> T[] toArray(T[] a) { // Estimate size of array; be prepared to see more or fewer elements int size = size(); T[] r = a.length >= size ? a : (T[])java.lang.reflect.Array .newInstance(a.getClass().getComponentType(), size); Iterator<E> it = iterator(); for (int i = 0; i < r.length; i++) { if (! it.hasNext()) { // fewer elements than expected if (a == r) { r[i] = null; // null-terminate } else if (a.length < i) { return Arrays.copyOf(r, i); } else { System.arraycopy(r, 0, a, 0, i); if (a.length > i) { a[i] = null; } } return a; } r[i] = (T)it.next(); } // more elements than expected return it.hasNext() ? finishToArray(r, it) : r; } 

ここで、配列はイテレータを使用してループで埋められます。これは、 Arrays.copyOf()およびSystem.arraycopy()を使用したデータ転送よりも遅くなります。 したがって、パフォーマンスを改善するには、 toArray()およびtoArray(T[])をオーバーライドし、 ArrayListと同じアプローチを使用する必要があります。 補足:


 private static class SubList<E> extends AbstractList<E> implements RandomAccess { private final ArrayList<E> root; private final SubList<E> parent; private final int offset; private int size; //... @Override public Object[] toArray() { return Arrays.copyOfRange(root.elementData, offset, offset + size); } @Override public <T> T[] toArray(T[] a) { if (a.length < size) return (T[]) Arrays.copyOfRange(root.elementData, offset, offset + size, a.getClass()); System.arraycopy(root.elementData, offset, a, 0, size); if (a.length > size) a[size] = null; return a; } //... } 

私たちはすべて正しいことをしましたか? いや! オーバーライドしたメソッドは、 subList()メソッドがsubList()れた後に元のリストが変更される可能性を考慮していません。 この機会を考慮に入れなければなりません。 したがって、オーバーライドされた各メソッドの先頭にチェックを追加します。


 @Override public Object[] toArray() { checkForComodification(); // <-- return Arrays.copyOfRange(root.elementData, offset, offset + size); } @Override public <T> T[] toArray(T[] a) { checkForComodification(); // <-- if (a.length < size) return (T[]) Arrays.copyOfRange(root.elementData, offset, offset + size, a.getClass()); System.arraycopy(root.elementData, offset, a, 0, size); if (a.length > size) a[size] = null; return a; } 

変更されたArrayList subList()ベンチマークsubList()と、 subList()メソッドのパフォーマンスはベースラインのパフォーマンスよりわずかに劣っていることがわかります。 わずかな遅れは、サブリストの作成と、 toArray(T[])メソッドの開始時のcheckForComodification()呼び出しによるものです。


ベンチマークサイズ得点エラー単位
リスト07.20.1ns / op
subList07.50.2ns / op
リスト1024.50.5ns / op
subList1025,40.6ns / op
リスト100142.84,5ns / op
subList100141.62.5ns / op
リスト10001243.628.5ns / op
subList10001247.823.7ns / op

乾燥残渣中:
チケットとパッチへのリンク (おそらくJava 11で終了します)


読むべきもの:
VMプールの黒魔術に関する長く複雑で非常に役立つ記事


メモのトピックに関する元の対応: ここにあります


結論



PSチケットはクローズされ、変更が注がれています。



Source: https://habr.com/ru/post/J348018/


All Articles