क्यों एक सॉर्ट किए गए सरणी * धीमे सरणी की तुलना में धीमी * प्रक्रिया कर रहा है? (जावा के ArrayList.indexOf)


80

शीर्षक के संदर्भ में यह है कि अनसोल्ड सरणी की तुलना में सॉर्ट किए गए सरणी को संसाधित करना अधिक तेज़ क्यों है?

यह एक शाखा भविष्यवाणी प्रभाव भी है? खबरदार: यहाँ छँटाई सरणी के लिए प्रसंस्करण धीमी है !!

निम्नलिखित कोड पर विचार करें:

private static final int LIST_LENGTH = 1000 * 1000;
private static final long SLOW_ITERATION_MILLIS = 1000L * 10L;

@Test
public void testBinarySearch() {
    Random r = new Random(0);
    List<Double> list = new ArrayList<>(LIST_LENGTH);
    for (int i = 0; i < LIST_LENGTH; i++) {
        list.add(r.nextDouble());
    }
    //Collections.sort(list);
    // remove possible artifacts due to the sorting call
    // and rebuild the list from scratch:
    list = new ArrayList<>(list);

    int nIterations = 0;
    long startTime = System.currentTimeMillis();
    do {
        int index = r.nextInt(LIST_LENGTH);
        assertEquals(index, list.indexOf(list.get(index)));
        nIterations++;
    } while (System.currentTimeMillis() < startTime + SLOW_ITERATION_MILLIS);
    long duration = System.currentTimeMillis() - startTime;
    double slowFindsPerSec = (double) nIterations / duration * 1000;
    System.out.println(slowFindsPerSec);

    ...
}

यह मेरी मशीन पर लगभग 720 के मूल्य को प्रिंट करता है।

अब अगर मैं संग्रह सॉर्ट कॉल को सक्रिय करता हूं, तो यह मूल्य 142 तक गिर जाता है। क्यों?

परिणाम हैं निर्णायक, अगर मैं पुनरावृत्तियों / समय की संख्या में वृद्धि वे नहीं बदलते।

जावा संस्करण 1.8.0_71 (ओरेकल वीएम, 64 बिट), विंडोज 10 के तहत चल रहा है, एक्लिप्स मंगल में जेनेट परीक्षण।

अपडेट करें

सन्निहित मेमोरी एक्सेस (यादृच्छिक क्रम में बनाम क्रमबद्ध रूप से एक्सेस की गई डबल ऑब्जेक्ट) से संबंधित प्रतीत होता है। प्रभाव मेरे लिए लगभग 10k और उससे कम की लंबाई लंबाई के लिए गायब हो जाता है।

परिणाम प्रदान करने के लिए assylias के लिए धन्यवाद :

/**
 * Benchmark                     Mode  Cnt  Score   Error  Units
 * SO35018999.shuffled           avgt   10  8.895 ± 1.534  ms/op
 * SO35018999.sorted             avgt   10  8.093 ± 3.093  ms/op
 * SO35018999.sorted_contiguous  avgt   10  1.665 ± 0.397  ms/op
 * SO35018999.unsorted           avgt   10  2.700 ± 0.302  ms/op
 */



3
यदि आप सार्थक परिणाम चाहते हैं तो जेएमएच जैसे उचित बेंचमार्किंग ढांचे के साथ अपने मापों को फिर से करें ।
क्लैशॉफ्ट

7
इसके अलावा, JMH के बिना भी आपकी परीक्षण पद्धति वैचारिक रूप से त्रुटिपूर्ण है। आप RNG सहित सभी प्रकार की चीजों का परीक्षण कर रहे हैं, System.currentTimeMillis और assertEquals। कोई वार्मअप पुनरावृत्तियां नहीं हैं, सामान्य रूप से पुनरावृत्तियां नहीं हैं, आप निरंतर समय पर भरोसा करते हैं और जांचते हैं कि उस समय में कितना किया गया था। क्षमा करें, लेकिन यह परीक्षा प्रभावी रूप से बेकार है।
क्लैशॉफ्ट

जवाबों:


88

यह कैशिंग / प्रीफेचिंग प्रभाव की तरह दिखता है।

सुराग यह है कि आप डबल्स (ऑब्जेक्ट्स) की तुलना करते हैं, डबल्स (आदिम) की नहीं। जब आप एक थ्रेड में ऑब्जेक्ट आवंटित करते हैं, तो उन्हें आम तौर पर मेमोरी में क्रमिक रूप से आवंटित किया जाता है। इसलिए जब indexOfएक सूची को स्कैन करता है, तो यह अनुक्रमिक मेमोरी पतों के माध्यम से जाता है। यह सीपीयू कैश प्रीफ़ेटिंग हेयुरिस्टिक्स के लिए अच्छा है।

लेकिन सूची को छाँटने के बाद, आपको अभी भी औसत रूप से मेमोरी लुकअप की संख्या करनी है, लेकिन इस बार मेमोरी एक्सेस यादृच्छिक क्रम में होगी।

अपडेट करें

यहां यह साबित करने के लिए बेंचमार्क है कि आवंटित वस्तुओं का क्रम मायने रखता है।

Benchmark            (generator)  (length)  (postprocess)  Mode  Cnt  Score   Error  Units
ListIndexOf.indexOf       random   1000000           none  avgt   10  1,243 ± 0,031  ms/op
ListIndexOf.indexOf       random   1000000           sort  avgt   10  6,496 ± 0,456  ms/op
ListIndexOf.indexOf       random   1000000        shuffle  avgt   10  6,485 ± 0,412  ms/op
ListIndexOf.indexOf   sequential   1000000           none  avgt   10  1,249 ± 0,053  ms/op
ListIndexOf.indexOf   sequential   1000000           sort  avgt   10  1,247 ± 0,037  ms/op
ListIndexOf.indexOf   sequential   1000000        shuffle  avgt   10  6,579 ± 0,448  ms/op

2
अगर यह सच है, तो छंटाई के बजाय फेरबदल करना एक ही परिणाम का उत्पादन करना चाहिए
डेविड सोरोको

1
@DavidSoroko करता है।
assylias

1
@DavidSoroko पूर्ण मानदंड के परिणाम के साथ अनचाहे, फेरबदल, क्रमबद्ध और क्रमबद्ध बेंचमार्क कोड के नीचे स्थित है ।
assylias

1
@assylias एक दिलचस्प एक्सटेंशन अनुक्रमिक संख्या भी बना सकता है (और परिणामस्वरूप कोड को पोस्ट करने से मेरा उत्तर अप्रचलित हो जाएगा)।
मार्को 13

1
बस पर जोर देना, में प्रीफ़ेचिंग के बाद से किसी भी तरह से लाभ नहीं होता यादृच्छिक है। मौसम की परवाह किए बिना सूची समान थी या नहीं की कीमत समान है। प्रीफ़ेटिंग केवल के लिए किक करता हैlist.indexOf(list.get(index))list.get(index)indexlist.get(index)list.indexOf()
डेविड सोरोको

25

मुझे लगता है कि हम मेमोरी कैश मिस का असर देख रहे हैं:

जब आप अनसुलझी सूची बनाते हैं

for (int i = 0; i < LIST_LENGTH; i++) {
    list.add(r.nextDouble());
}

सभी डबल एक सन्निहित स्मृति क्षेत्र में आवंटित किए जाने की संभावना है। इसके माध्यम से Iterating कुछ कैश मिस का उत्पादन करेगा।

दूसरी ओर क्रमबद्ध सूची में, संदर्भ अव्यवस्थित तरीके से स्मृति को इंगित करते हैं।

अब यदि आप सन्निहित स्मृति के साथ एक क्रमबद्ध सूची बनाते हैं:

Collection.sort(list);
List<Double> list2 = new ArrayList<>();
for (int i = 0; i < LIST_LENGTH; i++) {
    list2.add(new Double(list.get(i).doubleValue()));
}

इस क्रमबद्ध सूची में मूल एक (मेरे समय) की तुलना में समान प्रदर्शन है।


8

एक सरल उदाहरण के रूप में जो उत्तर की पुष्टि wero और apangin (+1!) द्वारा किया गया है: निम्नलिखित दोनों विकल्पों की एक सरल तुलना करता है:

  • यादृच्छिक संख्याएँ बनाना, और उन्हें वैकल्पिक रूप से क्रमबद्ध करना
  • अनुक्रमिक संख्याएं बनाना, और उन्हें वैकल्पिक रूप से फेरबदल करना

इसे जेएमएच बेंचमार्क के रूप में भी लागू नहीं किया गया है, लेकिन मूल कोड के समान, प्रभाव का निरीक्षण करने के लिए केवल मामूली संशोधनों के साथ:

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;

public class SortedListTest
{
    private static final long SLOW_ITERATION_MILLIS = 1000L * 3L;

    public static void main(String[] args)
    {
        int size = 100000;
        testBinarySearchOriginal(size, true);
        testBinarySearchOriginal(size, false);
        testBinarySearchShuffled(size, true);
        testBinarySearchShuffled(size, false);
    }

    public static void testBinarySearchOriginal(int size, boolean sort)
    {
        Random r = new Random(0);
        List<Double> list = new ArrayList<>(size);
        for (int i = 0; i < size; i++)
        {
            list.add(r.nextDouble());
        }
        if (sort)
        {
            Collections.sort(list);
        }
        list = new ArrayList<>(list);

        int count = 0;
        int nIterations = 0;
        long startTime = System.currentTimeMillis();
        do
        {
            int index = r.nextInt(size);
            if (index == list.indexOf(list.get(index)))
            {
                count++;
            }
            nIterations++;
        }
        while (System.currentTimeMillis() < startTime + SLOW_ITERATION_MILLIS);
        long duration = System.currentTimeMillis() - startTime;
        double slowFindsPerSec = (double) nIterations / duration * 1000;

        System.out.printf("Size %8d sort %5s iterations %10.3f count %10d\n",
            size, sort, slowFindsPerSec, count);
    }

    public static void testBinarySearchShuffled(int size, boolean sort)
    {
        Random r = new Random(0);
        List<Double> list = new ArrayList<>(size);
        for (int i = 0; i < size; i++)
        {
            list.add((double) i / size);
        }
        if (!sort)
        {
            Collections.shuffle(list);
        }
        list = new ArrayList<>(list);

        int count = 0;
        int nIterations = 0;
        long startTime = System.currentTimeMillis();
        do
        {
            int index = r.nextInt(size);
            if (index == list.indexOf(list.get(index)))
            {
                count++;
            }
            nIterations++;
        }
        while (System.currentTimeMillis() < startTime + SLOW_ITERATION_MILLIS);
        long duration = System.currentTimeMillis() - startTime;
        double slowFindsPerSec = (double) nIterations / duration * 1000;

        System.out.printf("Size %8d sort %5s iterations %10.3f count %10d\n",
            size, sort, slowFindsPerSec, count);
    }

}

मेरी मशीन पर आउटपुट है

Size   100000 sort  true iterations   8560,333 count      25681
Size   100000 sort false iterations  19358,667 count      58076
Size   100000 sort  true iterations  18554,000 count      55662
Size   100000 sort false iterations   8845,333 count      26536

अच्छी तरह से दिखा रहा है कि समय बिल्कुल दूसरे के विपरीत हैं: यदि यादृच्छिक संख्याओं को क्रमबद्ध किया जाता है, तो क्रमबद्ध संस्करण धीमा है। यदि अनुक्रमिक संख्याओं में फेरबदल किया जाता है, तो फेरबदल संस्करण धीमा होता है।

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.