Optimierte Zusammenführungssortierung schneller als Quicksort

Lesezeit: 10 Minuten

Optimierte Zusammenfuhrungssortierung schneller als Quicksort
ahitt6345

http://jsperf.com/optimized-mergesort-versus-quicksort

Warum funktioniert diese Half-Buffer-Merge-Sortierung so schnell wie Quicksort?

Quicksort ist:

  1. In-Place, obwohl es dauert log(n) Rekursionen (Stapelplatz)
  2. Cache-freundlich

Diese Half-Buffer-Merge-Sortierung:

  1. Verwendet ein n/2 Puffer für Zusammenführungen.
  2. Verwendet log(n) Rekursionen.
  3. Macht weniger Vergleiche.

Meine Frage ist, warum entspricht die Halbpuffer-Zusammenführungssortierung in diesem Szenario der Geschwindigkeit von QuickSort? Außerdem, mache ich irgendetwas falsch mit dem QuickSort, das es langsamer macht?

function partition(a, i, j) {
    var p = i + Math.floor((j - i) / 2);
    var left = i + 1;
    var right = j;
    swap(a, i, p);
    var pivot = a[i];
    while (left <= right) {
        while (builtinLessThan(a[left], pivot)) {
            ++left;
        }
        while (builtinLessThan(pivot, a[right])) {
            --right;
        }
        if (left <= right) {
            swap(a, left, right);
            ++left;
            --right;
        }
    }
    swap(a, i, right);
    return right;
};

function quickSort(a, i, j) {
    var p = partition(a, i, j);
    if ((p) - i > j - p) {
        if (i < p - 1) {
            quickSort(a, i, p - 1);
        }
        if (p + 1 < j) {
            quickSort(a, p + 1, j);
        }
    } else {
        if (p + 1 < j) {
            quickSort(a, p + 1, j);
        } if (i < p - 1) {
            quickSort(a, i, p - 1);
        }
    }
};

  • auf meiner Box ist es 30% langsamer.

    – Thomas Jungblut

    17. Januar 2016 um 22:42 Uhr

  • Nun, ich teste dies in Chrome und in Node. Im Knoten ist es doppelt so schnell wie Quicksort. In FireFox ist es 5% schneller als in Chrome.

    – ahitt6345

    17. Januar 2016 um 22:42 Uhr


  • Vergleiche sind billig. Optimiertes Quicksort macht ~2,5-mal weniger Swaps als Merge-Sort. Wenn also die Swap-Operation billig ist, ist die Zusammenführungssortierung schneller. Andernfalls gewinnt Quicksort.

    – SaschaMN

    17. Januar 2016 um 22:56 Uhr

  • Ich habe bereits teure Vergleiche getestet (Merge Sort war soooo viel schneller). Aber ich habe nie daran gedacht, teure (Swaps/Array-Zugriffe) zu testen. Irgendeine Idee, wie man das testet?

    – ahitt6345

    17. Januar 2016 um 23:24 Uhr

  • Der Link zu jsperf ist tot, daher kann ich nicht mehr sagen, was hier verglichen wird. Können wir den relevanten Code in diese Frage einfügen?

    – ggorlen

    18. September 2020 um 17:39 Uhr


1647144908 812 Optimierte Zusammenfuhrungssortierung schneller als Quicksort
rcgldr

Merge Sort führt weniger Vergleiche durch, aber mehr Moves als Quick Sort. Das Aufrufen einer Funktion zum Durchführen der Vergleiche erhöht den Overhead für Vergleiche, wodurch die schnelle Sortierung langsamer wird. All diese if-Anweisungen im Beispiel Quick Sort verlangsamen es auch. Wenn der Vergleich und der Austausch inline durchgeführt werden, sollte die schnelle Sortierung etwas schneller sein, wenn ein Array von pseudozufälligen Ganzzahlen sortiert wird.

Wenn auf einem Prozessor mit 16 Registern, wie einem PC im 64-Bit-Modus, ausgeführt wird, ist die 4-Wege-Zusammenführungssortierung mit einer Reihe von Zeigern, die in Registern landen, ungefähr so ​​​​schnell wie die schnelle Sortierung. Eine 2-Wege-Merge-Sortierung vergleicht durchschnittlich 1 für jedes verschobene Element, während eine 4-Wege-Merge-Sortierung durchschnittlich 3 Vergleiche für jedes verschobene Element durchführt, aber nur die Hälfte der Anzahl von Durchgängen benötigt, sodass die Anzahl der grundlegenden Operationen gleich ist, aber die Vergleiche sind etwas Cache-freundlicher, wodurch die 4-Wege-Merge-Sortierung etwa 15% schneller wird, ungefähr gleich viel wie die schnelle Sortierung.

Ich bin nicht mit Java-Skript vertraut, also konvertiere ich die Beispiele in C++.

Mit einer konvertierten Version des Java-Skripts Merge Sort dauert es etwa 2,4 Sekunden, um 16 Millionen pseudozufällige 32-Bit-Ganzzahlen zu sortieren. Die unten gezeigte beispielhafte schnelle Sortierung dauert etwa 1,4 Sekunden, und die unten gezeigte beispielhafte Zusammenführung von unten nach oben dauert etwa 1,6 Sekunden. Wie bereits erwähnt, würde ein 4-Wege-Merge mit einer Reihe von Zeigern (oder Indizes) auf einem Prozessor mit 16 Registern ebenfalls etwa 1,4 Sekunden dauern.

Beispiel für eine schnelle Sortierung in C++:

void QuickSort(int a[], int lo, int hi) {
    int i = lo, j = hi;
    int pivot = a[(lo + hi) / 2];
    int t;
    while (i <= j) {            // partition
        while (a[i] < pivot)
            i++;
        while (a[j] > pivot)
            j--;
        if (i <= j) {
            t = a[i]
            a[i] = a[j];
            a[j] = t;
            i++;
            j--;
        }
    }
    if (lo < j)                 // recurse
        QuickSort(a, lo, j);
    if (i < hi)
        QuickSort(a, i, hi);
}

C++-Bottom-Up-Merge-Sort-Beispiel:

void BottomUpMergeSort(int a[], int b[], size_t n)
{
size_t s = 1;                               // run size 
    if(GetPassCount(n) & 1){                // if odd number of passes
        for(s = 1; s < n; s += 2)           // swap in place for 1st pass
            if(a[s] < a[s-1])
                std::swap(a[s], a[s-1]);
        s = 2;
    }
    while(s < n){                           // while not done
        size_t ee = 0;                      // reset end index
        while(ee < n){                      // merge pairs of runs
            size_t ll = ee;                 // ll = start of left  run
            size_t rr = ll+s;               // rr = start of right run
            if(rr >= n){                    // if only left run
                rr = n;
                BottomUpCopy(a, b, ll, rr); //   copy left run
                break;                      //   end of pass
            }
            ee = rr+s;                      // ee = end of right run
            if(ee > n)
                ee = n;
            BottomUpMerge(a, b, ll, rr, ee);
        }
        std::swap(a, b);                    // swap a and b
        s <<= 1;                            // double the run size
    }
}

void BottomUpMerge(int a[], int b[], size_t ll, size_t rr, size_t ee)
{
    size_t o = ll;                          // b[]       index
    size_t l = ll;                          // a[] left  index
    size_t r = rr;                          // a[] right index
    while(1){                               // merge data
        if(a[l] <= a[r]){                   // if a[l] <= a[r]
            b[o++] = a[l++];                //   copy a[l]
            if(l < rr)                      //   if not end of left run
                continue;                   //     continue (back to while)
            while(r < ee)                   //   else copy rest of right run
                b[o++] = a[r++];
            break;                          //     and return
        } else {                            // else a[l] > a[r]
            b[o++] = a[r++];                //   copy a[r]
            if(r < ee)                      //   if not end of right run
                continue;                   //     continue (back to while)
            while(l < rr)                   //   else copy rest of left run
                b[o++] = a[l++];
            break;                          //     and return
        }
    }
}

void BottomUpCopy(int a[], int b[], size_t ll, size_t rr)
{
    while(ll < rr){                         // copy left run
        b[ll] = a[ll];
        ll++;
    }
}

size_t GetPassCount(size_t n)               // return # passes
{
    size_t i = 0;
    for(size_t s = 1; s < n; s <<= 1)
        i += 1;
    return(i);
}

C++-Beispiel für eine 4-Wege-Merge-Sortierung mit Zeigern (Goto wird verwendet, um Codeplatz zu sparen, es ist alter Code). Es beginnt mit dem 4-Wege-Merge, und wenn das Ende eines Laufs erreicht ist, wechselt es zum 3-Wege-Merge, dann zum 2-Wege-Merge und dann zu einer Kopie des Rests des verbleibenden Laufs. Dies ähnelt Algorithmen, die für externe Sortierungen verwendet werden, aber die externe Sortierungslogik ist allgemeiner und verarbeitet häufig bis zu 16-Wege-Zusammenführungen.

int * BottomUpMergeSort(int a[], int b[], size_t n)
{
int *p0r;       // ptr to      run 0
int *p0e;       // ptr to end  run 0
int *p1r;       // ptr to      run 1
int *p1e;       // ptr to end  run 1
int *p2r;       // ptr to      run 2
int *p2e;       // ptr to end  run 2
int *p3r;       // ptr to      run 3
int *p3e;       // ptr to end  run 3
int *pax;       // ptr to set of runs in a
int *pbx;       // ptr for merged output to b
size_t rsz = 1; // run size
    if(n < 2)
        return a;
    if(n == 2){
        if(a[0] > a[1])std::swap(a[0],a[1]);
        return a;
    }
    if(n == 3){
        if(a[0] > a[2])std::swap(a[0],a[2]);
        if(a[0] > a[1])std::swap(a[0],a[1]);
        if(a[1] > a[2])std::swap(a[1],a[2]);
        return a;
    }
    while(rsz < n){
        pbx = &b[0];
        pax = &a[0];
        while(pax < &a[n]){
            p0e = rsz + (p0r = pax);
            if(p0e >= &a[n]){
                p0e = &a[n];
                goto cpy10;}
            p1e = rsz + (p1r = p0e);
            if(p1e >= &a[n]){
                p1e = &a[n];
                goto mrg201;}
            p2e = rsz + (p2r = p1e);
            if(p2e >= &a[n]){
                p2e = &a[n];
                goto mrg3012;}
            p3e = rsz + (p3r = p2e);
            if(p3e >= &a[n])
                p3e = &a[n];
            // 4 way merge
            while(1){
                if(*p0r <= *p1r){
                    if(*p2r <= *p3r){
                        if(*p0r <= *p2r){
mrg40:                      *pbx++ = *p0r++;    // run 0 smallest
                            if(p0r < p0e)       // if not end run continue
                                continue;
                            goto mrg3123;       // merge 1,2,3
                        } else {
mrg42:                      *pbx++ = *p2r++;    // run 2 smallest
                            if(p2r < p2e)       // if not end run continue
                                continue;
                            goto mrg3013;       // merge 0,1,3
                        }
                    } else {
                        if(*p0r <= *p3r){
                            goto mrg40;         // run 0 smallext
                        } else {
mrg43:                      *pbx++ = *p3r++;    // run 3 smallest
                            if(p3r < p3e)       // if not end run continue
                                continue;
                            goto mrg3012;       // merge 0,1,2
                        }
                    }
                } else {
                    if(*p2r <= *p3r){
                        if(*p1r <= *p2r){
mrg41:                      *pbx++ = *p1r++;    // run 1 smallest
                            if(p1r < p1e)       // if not end run continue
                                continue;
                            goto mrg3023;       // merge 0,2,3
                        } else {
                            goto mrg42;         // run 2 smallest
                        }
                    } else {
                        if(*p1r <= *p3r){
                            goto mrg41;         // run 1 smallest
                        } else {
                            goto mrg43;         // run 3 smallest
                        }
                    }
                }
            }
            // 3 way merge
mrg3123:    p0r = p1r;
            p0e = p1e;
mrg3023:    p1r = p2r;
            p1e = p2e;
mrg3013:    p2r = p3r;
            p2e = p3e;
mrg3012:    while(1){
                if(*p0r <= *p1r){
                    if(*p0r <= *p2r){
                        *pbx++ = *p0r++;        // run 0 smallest
                        if(p0r < p0e)           // if not end run continue
                            continue;
                        goto mrg212;            // merge 1,2
                    } else {
mrg32:                  *pbx++ = *p2r++;        // run 2 smallest
                        if(p2r < p2e)           // if not end run continue
                            continue;
                        goto mrg201;            // merge 0,1
                    }
                } else {
                    if(*p1r <= *p2r){
                        *pbx++ = *p1r++;        // run 1 smallest
                        if(p1r < p1e)           // if not end run continue
                            continue;
                        goto mrg202;            // merge 0,2
                    } else {
                        goto mrg32;             // run 2 smallest
                    }
                }
            }
            // 2 way merge
mrg212:     p0r = p1r;
            p0e = p1e;
mrg202:     p1r = p2r;
            p1e = p2e;
mrg201:     while(1){
                if(*p0r <= *p1r){
                    *pbx++ = *p0r++;            // run 0 smallest
                    if(p0r < p0e)               // if not end run continue
                        continue;
                    goto cpy11;
                } else {
                    *pbx++ = *p1r++;            // run 1 smallest
                    if(p1r < p1e)               // if not end run continue
                        continue;
                    goto cpy10;
                }
            }
            // 1 way copy
cpy11:      p0r = p1r;
            p0e = p1e;
cpy10:      while (1) {
                *pbx++ = *p0r++;                // copy element
                if (p0r < p0e)                  // if not end of run continue
                    continue;
                break;
            }
            pax += rsz << 2;            // setup for next set of runs
        }
        std::swap(a, b);                // swap ptrs
        rsz <<= 2;                      // quadruple run size
    }
    return a;                           // return sorted array
}

  • Ich bin mit der obersten Aussage nicht einverstanden. Ich habe die aufgerufene Funktion verlassen compare Funktion in Mergesort und hat das Inline eingebaut > Vergleichsoperator nur im Quicksort. Obwohl es einen leichten Geschwindigkeitsschub gab (0,01 Sekunden Schub bei 200.000 Elementen), war es immer noch 2x langsamer als die Zusammenführungssortierung mit dem Funktionsvergleichs-Overhead.

    – ahitt6345

    18. Januar 2016 um 5:08 Uhr


  • Außerdem sind diese zusätzlichen if-Anweisungen Optimierungen. Eine der Quicksort-Optimierungen besteht darin, ZUERST auf die kleinere Seite der Partition zurückzugreifen. Das ist es, was all diese if-Anweisungen tun.

    – ahitt6345

    18. Januar 2016 um 5:10 Uhr

  • Außerdem wird die rekursive Merge-Sort-Implementierung verwendet. Das einzige, was optimiert wurde, war die merge Funktion.

    – ahitt6345

    18. Januar 2016 um 5:12 Uhr

  • @Royi – Die Zeiten scheinen sehr langsam zu sein. Ich habe einen 8 Jahre alten Computer (Intel 3770K, 3,5 GHz) und eine standardmäßige Zusammenführungssortierung für 100.000 64-Bit-Ganzzahlen ohne Vorzeichen dauert 6,45 ms, weniger als die Zeit, die in diesem Artikel für das Sortieren von 20.000 Elementen angegeben ist. Ich bin mir nicht sicher, ob 2 Bedingungen im Vergleich zu 1 in einer Schleife, die zwei Speichervergleiche beinhaltet, auch wenn sie zwischengespeichert sind, einen großen Unterschied machen werden. Die Verwendung von 5 für den Wechsel zur Einfügungssortierung scheint niedrig zu sein. Visual Studio-Vorlagen wechseln bei 32 Elementen. Andere Versionen schalten bei 32 oder 64 um, je nachdem, was zu einer geraden Anzahl von Durchgängen für die Zusammenführungssortierung von unten nach oben führt.

    – rcgldr

    13. Juli 2020 um 15:03 Uhr


  • @Royi – Die meisten Bibliotheken verwenden eine Variation einer hybriden Bottom-Up-Merge-Sortierung und Insertion-Sortierung. In meinen Versionen bestimme ich die Anzahl der Durchgänge für eine reine Zusammenführungssortierung. Wenn die Anzahl der Durchgänge ungerade wäre, verwende ich die Einfügungssortierung für Gruppen von 32 Elementen, sodass die Anzahl der Zusammenführungsdurchgänge gerade ist. Wenn die Anzahl der Durchgänge bereits gerade ist, verwende ich Insertion Sort für Gruppen von 64 Elementen, damit die Anzahl der Durchgänge gerade bleibt. Das Zusammenführen wechselt bei jedem Durchlauf die Richtung, wodurch das Zurückkopieren entfällt, und bei einer geraden Anzahl von Durchläufen landen die sortierten Daten im ursprünglichen Array.

    – rcgldr

    13. Juli 2020 um 18:27 Uhr

995900cookie-checkOptimierte Zusammenführungssortierung schneller als Quicksort

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy