8

私は、コレクションやコレクションのコレクションなどを検索するさまざまな方法をいじくり回してきました。私の理解を確認するために、愚かな小さなテストをたくさん行っています。これが私を困惑させるものです(ソースコードはさらに下にあります)。

つまり、N個のランダムな整数を生成し、それらをリストに追加しています。リストはソートされていません。次に、を使用Collections.contains()してリスト内の値を検索します。リストスペース全体がプローブされるようにしたいので、意図的にそこにないことがわかっている値を探します。私はこの検索の時間を計ります。

次に、別の線形検索を手動で実行し、リストの各要素を繰り返し処理して、ターゲットと一致するかどうかを確認します。私もこの検索の時間を計ります。

平均して、2回目の検索は最初の検索より33%長くかかります。私の論理では、リストはソートされていないため、最初の検索も線形である必要があります。私が考えることができる唯一の可能性(私はすぐに破棄します)は、Javaが検索のためだけにリストのソートされたコピーを作成しているということですが、(1)メモリスペースの使用を許可していませんでした。このような大きなNを使用すると、大幅な時間の節約になります。

したがって、両方の検索が線形である場合、両方とも同じ時間がかかるはずです。どういうわけか、Collectionsクラスはこの検索を最適化しましたが、その方法がわかりません。だから...私は何が欠けていますか?

import java.util.*;

public class ListSearch {

    public static void main(String[] args) {

        int N = 10000000; // number of ints to add to the list
        int high = 100; // upper limit for random int generation

        List<Integer> ints;
        int target = -1; // target will not be found, forces search of entire list space

        long start;
        long end;

        ints = new ArrayList<Integer>();
        start = System.currentTimeMillis();
        System.out.print("Generating new list... ");
        for (int i = 0; i < N; i++) {
            ints.add(((int) (Math.random() * high)) + 1);
        }
        end = System.currentTimeMillis();
        System.out.println("took "  + (end-start) + "ms.");
        start = System.currentTimeMillis();
        System.out.print("Searching list for target (method 1)... ");
        if (ints.contains(target)) {
            // nothing
        }
        end = System.currentTimeMillis();
        System.out.println(" Took "  + (end-start) + "ms.");

        System.out.println();

        ints = new ArrayList<Integer>();
        start = System.currentTimeMillis();
        System.out.print("Generating new list... ");
        for (int i = 0; i < N; i++) {
            ints.add(((int) (Math.random() * high)) + 1);
        }
        end = System.currentTimeMillis();
        System.out.println("took "  + (end-start) + "ms.");
        start = System.currentTimeMillis();
        System.out.print("Searching list for target (method 2)... ");
        for (Integer i : ints) {
            // nothing
        }
        end = System.currentTimeMillis();
        System.out.println(" Took "  + (end-start) + "ms.");
    }
}

編集:以下はこのコードの新しいバージョンです。興味深いのは、手動の線形ループがメソッドよりも16%高速に実行されるcontainsことです(注:どちらも意図的にリストスペース全体を検索するように設計されているため、反復回数が等しいことがわかります)。私はこの16%の利益を説明することはできません...もっと混乱します。

import java.util.*;

public class ListSearch {

    public static void main(String[] args) {

        int N = 10000000; // number of ints to add to the list
        int high = 100; // upper limit for random int generation

        List<Integer> ints;
        int target = -1; // target will not be found, forces search of entire list space

        long start;
        long end;

        ints = new ArrayList<Integer>();
        start = System.currentTimeMillis();
        System.out.print("Generating new list... ");
        for (int i = 0; i < N; i++) {
            ints.add(((int) (Math.random() * high)) + 1);
        }
        end = System.currentTimeMillis();
        System.out.println("took "  + (end-start) + "ms.");
        start = System.currentTimeMillis();
        System.out.print("Searching list for target (method 1)... ");
        if (ints.contains(target)) {
            System.out.println("hit");
        }
        end = System.currentTimeMillis();
        System.out.println(" Took "  + (end-start) + "ms.");

        System.out.println();

        ints = new ArrayList<Integer>();
        start = System.currentTimeMillis();
        System.out.print("Generating new list... ");
        for (int i = 0; i < N; i++) {
            ints.add(((int) (Math.random() * high)) + 1);
        }
        end = System.currentTimeMillis();
        System.out.println("took "  + (end-start) + "ms.");
        start = System.currentTimeMillis();
        System.out.print("Searching list for target (method 2)... ");
        for (int i = 0; i < N; i++) {
            if (ints.get(i) == target) {
                System.out.println("hit");
            }
        }
        end = System.currentTimeMillis();
        System.out.println(" Took "  + (end-start) + "ms.");
    }
}
4

3 に答える 3

6

比較コードにバグがあり、結果が歪んでいます。

これはtarget

    if (ints.contains(target)) {
        // nothing
    }

しかし、これはそうではありません!

    for (Integer i : ints) {
        // nothing
    }

実際には、リスト要素をテストせずに繰り返し処理しているだけです。

そうは言っても、次の理由の 1 つ以上により、2 番目のバージョンは最初のバージョンよりも遅くなります。

  • for最初のバージョンでは、単純なループとインデックスを使用してバッキング配列を反復します。2 番目のバージョンは、次のものと同等です。

    Iterator<Integer> it = ints.iterator();
    while (it.hasNext()) {
        Integer i = (Integer) it.next();
    }
    

    つまり、ループのたびに 2 つのメソッド呼び出しと型キャスト1が含まれます。

  • 最初のバージョンはtrue、一致するとすぐに返されます。実装のバグにより、2 番目のバージョンは毎回リスト全体を反復します。実際、 と の選択を考えるNと、この効果がパフォーマンスの違いの主なhigh原因である可能性が最も高くなります。

1 - 実際には、JIT コンパイラーがこれらすべてに対して何を行うかは完全には明らかではありません。理論的には、メソッド呼び出しをインライン化したり、typcaset が不要であると推測したり、ループ全体を最適化することさえできます。一方で、これらの最適化を阻害する要因もあります。たとえば、インライン化を禁止する可能性があるintsと宣言されてList<Integer>います...実際の型が常に同じであるとJITが推測できない限り。


別の理由で結果が歪んでいる可能性もあります。あなたのコードは JVM ウォームアップを考慮していません。詳細については、この質問をお読みください: How do I write a correct micro-benchmark in Java?

于 2012-10-20T05:30:21.190 に答える
3

違いは次のとおりです。

を使用するcontainsと、オブジェクトの内部配列が使用され、次のような検索が行われます。

    for (int i = 0; i < size; i++)
        if (searchObject.equals(listObject[i]))
            return true;
    return false;

ここで要素を取得しようとするとith、内部配列から i 番目の要素オブジェクトを直接取得します。

自分で書くときは、次のように書きます。

    for (Integer i : ints) {
        // nothing
    }

と同等:

   for(Iterator<Integer> iter= ints.iterator(); iter.hasNext(); ) {
         Integer i = iter.next();
   }

よりもはるかに多くのステップを実行していますcontains

于 2012-10-20T04:52:32.147 に答える
1

したがって、あなたが何かをテストしているとは完全にはわかりません。Javac (コンパイラ) は、for ループと if ステートメント内にコードがないことを認識できるほどスマートです。この場合、Java はそのコードをコンパイルから削除します。時間が戻ってくる理由は、文字列を出力するのにかかる時間を実際に数えているからです。システムの出力時間は、システムの実行内容によって大幅に異なる場合があります。タイミング テストを記述する場合、I/O によって無効なテストが作成される可能性があります。

最初に、タイミング内からストリング プリントを削除します。

第二に、ArrayList.contains は線形です。あなたがしているような特別な for ループは使用しません。ループには、コレクションから反復子を取得してから反復処理するというオーバーヘッドがあります。これが、特殊な for ループが舞台裏でどのように機能するかです。

お役に立てれば。

于 2012-10-20T04:59:13.257 に答える