web-dev-qa-db-ja.com

なぜJavaのArrayList削除関数はそれほどコストがかからないように見えるのですか?

約250,000を超える非常に大きなリストを操作する機能があります。これらのアイテムの大部分については、位置xのアイテムを単純に置き換えます。ただし、それらの約5%については、リストから削除する必要があります。

LinkedListを使用することは、高価な削除を回避するための最も明白な解決策であると思われました。ただし、当然のことながら、インデックスによるLinkedListへのアクセスは、時間が経過するにつれてますます遅くなります。ここでのコストは数分です(そしてそれらの多く)。

そのLinkedListでIteratorを使用するのもコストがかかります。リストの編集中にIteratorの同時実行の問題を回避するために別のコピーが必要と思われるためです。ここでのコストは数分です。

しかし、ここで私の心が少し吹き飛ばされます。 ArrayListに変更すると、ほぼ瞬時に実行されます。

297515個の要素を含むリストの場合、11958個の要素を削除して他のすべてを変更するには909msかかります。結果のリストのサイズが予想どおり285557であり、必要な更新情報が含まれていることを確認しました。

なぜこんなに速いのですか? JDK6でArrayListのソースを見たところ、予想どおりarraycopy関数を使用しているようです。常識がこのタスクの配列がひどいアイデアであり、数十万個のアイテムをシフトする必要があることを示しているように見えるときに、ここでArrayListがうまく機能する理由を理解したいと思います。

55
Ken

リスト要素をフィルタリングするために次の各戦略を試して、ベンチマークを実行しました。

  • 必要な要素を新しいリストにコピーします
  • Iterator.remove()を使用して、ArrayListから不要な要素を削除します
  • LinkedListから不要な要素を削除するには、Iterator.remove()を使用します
  • リストをその場で圧縮する(必要な要素を低い位置に移動する)
  • ArrayListのインデックス(List.remove(int))で削除
  • LinkedListのインデックス(List.remove(int))で削除

リストにPointのランダムなインスタンスを100000個作成し、要素の95%を受け入れ、残りの5%を拒否するフィルター条件(ハッシュコードに基づく)を使用するたびに(質問に記載された割合と同じ) 、ただし、250000要素のテストを実行する時間がなかったため、リストは小さくなりました。)

そして、平均時間(私の古いMacBook Pro:Core 2 Duo、2.2GHz、3Gb RAM)は:

CopyIntoNewListWithIterator   :      4.24ms
CopyIntoNewListWithoutIterator:      3.57ms
FilterLinkedListInPlace       :      4.21ms
RandomRemoveByIndex           :    312.50ms
SequentialRemoveByIndex       :  33632.28ms
ShiftDown                     :      3.75ms

そのため、LinkedListからインデックスで要素を削除することは、ArrayListから要素を削除するよりも300倍以上高価であり、おそらく他の方法よりも6000-10000倍高い検索およびarraycopy

ここでは、4つの高速なメソッドに大きな違いはないようですが、500000要素のリストを使用してこれら4つのメソッドを再度実行すると、次の結果が得られました。

CopyIntoNewListWithIterator   :     92.49ms
CopyIntoNewListWithoutIterator:     71.77ms
FilterLinkedListInPlace       :     15.73ms
ShiftDown                     :     11.86ms

大きなサイズのキャッシュメモリが制限要因になるため、リストの2番目のコピーを作成するコストが大きくなると思います。

コードは次のとおりです。

import Java.awt.Point;
import Java.security.SecureRandom;
import Java.util.ArrayList;
import Java.util.Arrays;
import Java.util.Collection;
import Java.util.Iterator;
import Java.util.LinkedList;
import Java.util.List;
import Java.util.Map;
import Java.util.Random;
import Java.util.TreeMap;

public class ListBenchmark {

    public static void main(String[] args) {
        Random rnd = new SecureRandom();
        Map<String, Long> timings = new TreeMap<String, Long>();
        for (int outerPass = 0; outerPass < 10; ++ outerPass) {
            List<FilterStrategy> strategies =
                Arrays.asList(new CopyIntoNewListWithIterator(),
                              new CopyIntoNewListWithoutIterator(),
                              new FilterLinkedListInPlace(),
                              new RandomRemoveByIndex(),
                              new SequentialRemoveByIndex(),
                              new ShiftDown());
            for (FilterStrategy strategy: strategies) {
                String strategyName = strategy.getClass().getSimpleName();
                for (int innerPass = 0; innerPass < 10; ++ innerPass) {
                    strategy.populate(rnd);
                    if (outerPass >= 5 && innerPass >= 5) {
                        Long totalTime = timings.get(strategyName);
                        if (totalTime == null) totalTime = 0L;
                        timings.put(strategyName, totalTime - System.currentTimeMillis());
                    }
                    Collection<Point> filtered = strategy.filter();
                    if (outerPass >= 5 && innerPass >= 5) {
                        Long totalTime = timings.get(strategyName);
                        timings.put(strategy.getClass().getSimpleName(), totalTime + System.currentTimeMillis());
                    }
                    CHECKSUM += filtered.hashCode();
                    System.err.printf("%-30s %d %d %d%n", strategy.getClass().getSimpleName(), outerPass, innerPass, filtered.size());
                    strategy.clear();
                }
            }
        }
        for (Map.Entry<String, Long> e: timings.entrySet()) {
            System.err.printf("%-30s: %9.2fms%n", e.getKey(), e.getValue() * (1.0/25.0));
        }
    }

    public static volatile int CHECKSUM = 0;

    static void populate(Collection<Point> dst, Random rnd) {
        for (int i = 0; i < INITIAL_SIZE; ++ i) {
            dst.add(new Point(rnd.nextInt(), rnd.nextInt()));
        }
    }

    static boolean wanted(Point p) {
        return p.hashCode() % 20 != 0;
    }

    static abstract class FilterStrategy {
        abstract void clear();
        abstract Collection<Point> filter();
        abstract void populate(Random rnd);
    }

    static final int INITIAL_SIZE = 100000;

    private static class CopyIntoNewListWithIterator extends FilterStrategy {
        public CopyIntoNewListWithIterator() {
            list = new ArrayList<Point>(INITIAL_SIZE);
        }
        @Override
        void clear() {
            list.clear();
        }
        @Override
        Collection<Point> filter() {
            ArrayList<Point> dst = new ArrayList<Point>(list.size());
            for (Point p: list) {
                if (wanted(p)) dst.add(p);
            }
            return dst;
        }
        @Override
        void populate(Random rnd) {
            ListBenchmark.populate(list, rnd);
        }
        private final ArrayList<Point> list;
    }

    private static class CopyIntoNewListWithoutIterator extends FilterStrategy {
        public CopyIntoNewListWithoutIterator() {
            list = new ArrayList<Point>(INITIAL_SIZE);
        }
        @Override
        void clear() {
            list.clear();
        }
        @Override
        Collection<Point> filter() {
            int inputSize = list.size();
            ArrayList<Point> dst = new ArrayList<Point>(inputSize);
            for (int i = 0; i < inputSize; ++ i) {
                Point p = list.get(i);
                if (wanted(p)) dst.add(p);
            }
            return dst;
        }
        @Override
        void populate(Random rnd) {
            ListBenchmark.populate(list, rnd);
        }
        private final ArrayList<Point> list;
    }

    private static class FilterLinkedListInPlace extends FilterStrategy {
        public String toString() {
            return getClass().getSimpleName();
        }
        FilterLinkedListInPlace() {
            list = new LinkedList<Point>();
        }
        @Override
        void clear() {
            list.clear();
        }
        @Override
        Collection<Point> filter() {
            for (Iterator<Point> it = list.iterator();
                 it.hasNext();
                 ) {
                Point p = it.next();
                if (! wanted(p)) it.remove();
            }
            return list;
        }
        @Override
        void populate(Random rnd) {
            ListBenchmark.populate(list, rnd);
        }
        private final LinkedList<Point> list;
    }

    private static class RandomRemoveByIndex extends FilterStrategy {
        public RandomRemoveByIndex() {
            list = new ArrayList<Point>(INITIAL_SIZE);
        }
        @Override
        void clear() {
            list.clear();
        }
        @Override
        Collection<Point> filter() {
            for (int i = 0; i < list.size();) {
                if (wanted(list.get(i))) {
                    ++ i;
                } else {
                    list.remove(i);
                }
            }
            return list;
        }
        @Override
        void populate(Random rnd) {
            ListBenchmark.populate(list, rnd);
        }
        private final ArrayList<Point> list;
    }

    private static class SequentialRemoveByIndex extends FilterStrategy {
        public SequentialRemoveByIndex() {
            list = new LinkedList<Point>();
        }
        @Override
        void clear() {
            list.clear();
        }
        @Override
        Collection<Point> filter() {
            for (int i = 0; i < list.size();) {
                if (wanted(list.get(i))) {
                    ++ i;
                } else {
                    list.remove(i);
                }
            }
            return list;
        }
        @Override
        void populate(Random rnd) {
            ListBenchmark.populate(list, rnd);
        }
        private final LinkedList<Point> list;
    }

    private static class ShiftDown extends FilterStrategy {
        public ShiftDown() {
            list = new ArrayList<Point>();
        }
        @Override
        void clear() {
            list.clear();
        }
        @Override
        Collection<Point> filter() {
            int inputSize = list.size();
            int outputSize = 0;
            for (int i = 0; i < inputSize; ++ i) {
                Point p = list.get(i);
                if (wanted(p)) {
                    list.set(outputSize++, p);
                }
            }
            list.subList(outputSize, inputSize).clear();
            return list;
        }
        @Override
        void populate(Random rnd) {
            ListBenchmark.populate(list, rnd);
        }
        private final ArrayList<Point> list;
    }

}
29
finnw

配列コピーはかなり安価な操作です。これは非常に基本的なレベル(Javaネイティブスタティックメソッド)で行われますが、パフォーマンスが本当に重要になる範囲にはまだ入っていません。

この例では、サイズが150000(平均)の配列の約12000倍をコピーします。これには時間がかかりません。ここでラップトップでテストしましたが、500ミリ秒もかかりませんでした。

更新次のコードを使用してラップトップで測定しました(Intel P8400)

import Java.util.Random;

public class PerformanceArrayCopy {

    public static void main(String[] args) {

        int[] lengths = new int[] { 10000, 50000, 125000, 250000 };
        int[] loops = new int[] { 1000, 5000, 10000, 20000 };

        for (int length : lengths) {
            for (int loop : loops) {

                Object[] list1 = new Object[length];
                Object[] list2 = new Object[length];

                for (int k = 0; k < 100; k++) {
                    System.arraycopy(list1, 0, list2, 0, list1.length);
                }

                int[] len = new int[loop];
                int[] ofs = new int[loop];

                Random rnd = new Random();
                for (int k = 0; k < loop; k++) {
                    len[k] = rnd.nextInt(length);
                    ofs[k] = rnd.nextInt(length - len[k]);
                }

                long n = System.nanoTime();
                for (int k = 0; k < loop; k++) {
                    System.arraycopy(list1, ofs[k], list2, ofs[k], len[k]);
                }
                n = System.nanoTime() - n;
                System.out.print("length: " + length);
                System.out.print("\tloop: " + loop);
                System.out.print("\truntime [ms]: " + n / 1000000);
                System.out.println();
            }
        }
    }
}

いくつかの結果:

length: 10000   loop: 10000 runtime [ms]: 47
length: 50000   loop: 10000 runtime [ms]: 228
length: 125000  loop: 10000 runtime [ms]: 575
length: 250000  loop: 10000 runtime [ms]: 1198
17
Howard

パフォーマンスの違いは、ArrayListがLinkedListがサポートしないランダムアクセスをサポートするという違いに起因していると思われます。

ArrayListのget(1000)が必要な場合、これにアクセスする特定のインデックスを指定していますが、LinkedListはNode参照によって編成されているため、これをサポートしていません。

LinkedListのget(1000)を呼び出すと、インデックス1000が見つかるまでリスト全体が繰り返されます。LinkedListに多数のアイテムがある場合、これは途方もなく高価になる可能性があります。

10
maple_shaft

基本的な違いを説明するために、ここでは意図的にいくつかの実装の詳細をスキップしています。

M個の要素のリストのN番目の要素を削除するには、LinkedList実装がこの要素まで移動し、それを削除して、N-1およびN + 1要素のポインタを適宜更新します。この2番目の操作は非常に簡単ですが、この要素に近づいているため、時間がかかります。

ただし、ArrayListの場合、アクセス時間は配列に連動しているため瞬時に発生します。これは、連続したメモリ空間を意味します。適切なメモリアドレスに直接ジャンプして、大まかに言うと、次のことを実行できます。

  • m-1要素の新しい配列を再割り当てします
  • 新しい配列リストの配列のインデックス0に0〜N-1のすべてを配置します
  • arraylistの配列のインデックスNにN + 1からMまでのすべてを配置します。

それを考えると、同じ配列をJavaは事前に割り当てられたサイズのArrayListを使用できるため、要素を削除する場合は手順1と2をスキップすることもできます。手順3を直接実行し、サイズを更新します。

メモリアクセスは高速であり、メモリのチャンクのコピーはおそらく現代のハードウェアでは十分に高速であるため、N位置への移動には時間がかかりすぎます。

ただし、LinkedListを使用して、互いに続く複数の要素を削除し、自分の位置を追跡できるようにする場合、ゲインが表示されます。

しかし、明らかに、長いリストでは、単純なremove(i)の実行にはコストがかかります。


これに塩と香辛料を追加するには:

  • 配列データ構造の効率に関する注意 および 動的配列のパフォーマンスに関する注意Wikipedia エントリを参照してください。
  • 連続メモリを必要とするメモリ構造を使用するには、連続メモリが必要であることに注意してください。つまり、仮想メモリは連続したチャンクを割り当てることができる必要があります。または、Javaを使用している場合でも、低レベルのクラッシュで原因となっているあいまいなOutOfMemoryExceptionにより、JVMが問題なくダウンしていることがわかります。
6
haylem

興味深い予想外の結果。これは単なる仮説ですが、...

平均して、配列要素の削除の1つでは、リストの半分(その後のすべて)を1つの要素に戻す必要があります。各アイテムがオブジェクトへの64ビットポインター(8バイト)である場合、これは125000アイテムxポインターあたり8バイト= 1 MBをコピーすることを意味します。

最新のCPUは、1 MBの連続ブロックをRAMにRAMかなり迅速にコピーできます。

比較や分岐、その他のCPUにやさしいアクティビティを必要とするアクセスごとにリンクリストをループするのに比べて、RAMコピーは高速です。

さまざまな操作を個別にベンチマークし、さまざまなリスト実装でどれだけ効率的かを確認する必要があります。もしそうなら、ここで結果を共有してください!

6
dkamins