Il modo più veloce per ordinare 10 numeri? (i numeri sono a 32 bit)


211

Sto risolvendo un problema e comporta l'ordinamento di 10 numeri (int32) molto rapidamente. La mia applicazione deve ordinare 10 numeri milioni di volte il più velocemente possibile. Sto campionando un insieme di dati di miliardi di elementi e ogni volta ho bisogno di selezionarne 10 numeri (semplificati) e ordinarli (e trarre conclusioni dall'elenco dei 10 elementi ordinati).

Attualmente sto usando l'ordinamento di inserzione ma immagino di poter implementare un algoritmo di ordinamento personalizzato molto veloce per il mio problema specifico di 10 numeri che batterebbe l'ordinamento di inserzione.

Qualcuno ha idea di come affrontare questo problema?


14
Per quanto rozzo sembri, una serie di ifaffermazioni nidificate dovrebbe funzionare al meglio. Evita i loop.
John Alexiou,

8
Ti aspetti che i numeri ti saranno dati con qualche pregiudizio nell'insieme delle permutazioni o saranno distribuiti uniformemente? Ci sarà una relazione tra l'ordinamento di un elenco e quello successivo?
Douglas Zare,

4
L'intero set di dati (con miliardi di numeri) è distribuito secondo la legge di Benford ma quando scelgo gli elementi casualmente da questo set, non lo sono più (penso).
bodacydo,

13
Si consiglia di leggere questo stackoverflow.com/q/2786899/995714
phuclv

11
Se selezioni in modo casuale tra miliardi di elementi, è possibile che la latenza per l'estrazione di tali dati abbia un impatto maggiore rispetto al tempo necessario per ordinare gli elementi selezionati anche se l'intero set di dati è nella RAM. È possibile testare l'impatto confrontando le prestazioni selezionando i dati in modo sequenziale anziché casuale.
Steve S.

Risposte:


213

(Seguendo il suggerimento di HelloWorld di esaminare le reti di smistamento.)

Sembra che una rete 29-compare / swap sia il modo più veloce per fare un ordinamento a 10 input. Ho usato la rete scoperta da Waksman nel 1969 per questo esempio in Javascript, che dovrebbe tradursi direttamente in C, poiché è solo un elenco di ifistruzioni, confronti e scambi.

function sortNet10(data) {	// ten-input sorting network by Waksman, 1969
    var swap;
    if (data[0] > data[5]) { swap = data[0]; data[0] = data[5]; data[5] = swap; }
    if (data[1] > data[6]) { swap = data[1]; data[1] = data[6]; data[6] = swap; }
    if (data[2] > data[7]) { swap = data[2]; data[2] = data[7]; data[7] = swap; }
    if (data[3] > data[8]) { swap = data[3]; data[3] = data[8]; data[8] = swap; }
    if (data[4] > data[9]) { swap = data[4]; data[4] = data[9]; data[9] = swap; }
    if (data[0] > data[3]) { swap = data[0]; data[0] = data[3]; data[3] = swap; }
    if (data[5] > data[8]) { swap = data[5]; data[5] = data[8]; data[8] = swap; }
    if (data[1] > data[4]) { swap = data[1]; data[1] = data[4]; data[4] = swap; }
    if (data[6] > data[9]) { swap = data[6]; data[6] = data[9]; data[9] = swap; }
    if (data[0] > data[2]) { swap = data[0]; data[0] = data[2]; data[2] = swap; }
    if (data[3] > data[6]) { swap = data[3]; data[3] = data[6]; data[6] = swap; }
    if (data[7] > data[9]) { swap = data[7]; data[7] = data[9]; data[9] = swap; }
    if (data[0] > data[1]) { swap = data[0]; data[0] = data[1]; data[1] = swap; }
    if (data[2] > data[4]) { swap = data[2]; data[2] = data[4]; data[4] = swap; }
    if (data[5] > data[7]) { swap = data[5]; data[5] = data[7]; data[7] = swap; }
    if (data[8] > data[9]) { swap = data[8]; data[8] = data[9]; data[9] = swap; }
    if (data[1] > data[2]) { swap = data[1]; data[1] = data[2]; data[2] = swap; }
    if (data[3] > data[5]) { swap = data[3]; data[3] = data[5]; data[5] = swap; }
    if (data[4] > data[6]) { swap = data[4]; data[4] = data[6]; data[6] = swap; }
    if (data[7] > data[8]) { swap = data[7]; data[7] = data[8]; data[8] = swap; }
    if (data[1] > data[3]) { swap = data[1]; data[1] = data[3]; data[3] = swap; }
    if (data[4] > data[7]) { swap = data[4]; data[4] = data[7]; data[7] = swap; }
    if (data[2] > data[5]) { swap = data[2]; data[2] = data[5]; data[5] = swap; }
    if (data[6] > data[8]) { swap = data[6]; data[6] = data[8]; data[8] = swap; }
    if (data[2] > data[3]) { swap = data[2]; data[2] = data[3]; data[3] = swap; }
    if (data[4] > data[5]) { swap = data[4]; data[4] = data[5]; data[5] = swap; }
    if (data[6] > data[7]) { swap = data[6]; data[6] = data[7]; data[7] = swap; }
    if (data[3] > data[4]) { swap = data[3]; data[3] = data[4]; data[4] = swap; }
    if (data[5] > data[6]) { swap = data[5]; data[5] = data[6]; data[6] = swap; }
    return(data);
}

alert(sortNet10([5,7,1,8,4,3,6,9,2,0]));

Ecco una rappresentazione grafica della rete, suddivisa in fasi indipendenti. Per sfruttare l'elaborazione parallela, il raggruppamento 5-4-3-4-4-4-3-2 può essere modificato in un raggruppamento 4-4-4-4-4-4-3-2.
Rete di smistamento a 10 ingressi (Waksman, 1969)

Rete di smistamento a 10 ingressi (Waksman, 1969) raggruppata nuovamente


69
suggerimento; usa una macro di scambio. come#define SORTPAIR(data, i1, i2) if (data[i1] > data[i2]) { int swap = data[i1]... }
Peter Cordes,

9
Si può dimostrare logicamente che questo è il minimo?
corsiKa

8
@corsiKa Sì, le reti di smistamento sono state un'area di ricerca sin dai primi tempi dell'informatica. In molti casi, le soluzioni ottimali sono note da decenni. Vedi en.wikipedia.org/wiki/Sorting_network
m69 '' snarky and unwelcoming ''

8
Ho testato un Jsperf e posso confermare che l'ordinamento di rete è oltre 20 volte più veloce dell'ordinamento nativo dei browser. jsperf.com/fastest-10-number-sort
Daniel

9
@Katai Questo distruggerebbe qualsiasi ottimizzazione che il tuo compilatore potesse produrre. Cattiva idea. Leggi questo per maggiori informazioni en.wikipedia.org/wiki/…
Antzi

88

Quando hai a che fare con questa dimensione fissa, dai un'occhiata a Ordinamento di reti . Questi algoritmi hanno un runtime fisso e sono indipendenti dal loro input. Per il tuo caso d'uso non hai un tale sovraccarico che alcuni algoritmi di ordinamento hanno.

L'ordinamento bitonico è un'implementazione di tale rete. Questo funziona meglio con len (n) <= 32 su una CPU. Su input più grandi potresti pensare di passare a una GPU. https://en.wikipedia.org/wiki/Sorting_network

A proposito, una buona pagina per confrontare gli algoritmi di ordinamento è questa qui (anche se manca il bitonic sort.

http://www.sorting-algorithms.com


3
@ ErickG.Hagstrom Ci sono molte soluzioni; fintanto che usano 29 confronti, sono ugualmente efficienti. Ho usato la soluzione di Waksman dal 1969; apparentemente fu il primo a scoprire una versione di 29 confronti.
m69 '' accattivante e poco accogliente ''

1
Sì, @ m69. Ce ne sono oltre un milione. La soluzione di Waksman ha una lunghezza di 29 e una profondità di 9. La soluzione che ho collegato è un miglioramento rispetto a quello nella dimensione della profondità: lunghezza = 29, profondità = 8. Naturalmente, se implementata in C, la profondità non ha importanza.
Erick G. Hagstrom,

4
@ ErickG.Hagstrom Apparentemente ci sono 87 soluzioni con profondità 7, la prima delle quali è stata trovata da Knuth nel 1973, ma non sono riuscito a trovarne nessuna con un veloce Google. larc.unt.edu/ian/pubs/9-input.pdf (vedi Conclusione, p.14)
m69 '' snarky and unselcoming ''

4
@ ErickG.Hagstrom: la profondità potrebbe non fare alcuna differenza "a livello di C", ma presumibilmente una volta che il compilatore e la CPU hanno terminato, ci sono alcune possibilità che sarà parzialmente parallelizzato all'interno della CPU e quindi una profondità minore potrebbe aiutare. A seconda della CPU, ovviamente: alcune CPU sono relativamente semplici e fanno una cosa dopo l'altra, mentre alcune CPU possono eseguire più operazioni in volo, in particolare potresti ottenere prestazioni molto diverse per tutti i carichi e gli archivi nello stack necessari in per manipolare 10 variabili, a seconda di come sono state fatte.
Steve Jessop,

1
@ ErickG.Hagstrom Non è stato immediatamente chiaro dall'articolo di Ian Parberry, ma le reti di profondità 7 hanno una lunghezza maggiore di 29. Vedi Knuth, "L'arte della programmazione informatica Vol.III", §5.3.4, fig . 49 e 51.
m69 '' accattivante e poco accogliente ''

33

Utilizzare una rete di ordinamento con confronti in gruppi di 4, quindi è possibile farlo nei registri SIMD. Una coppia di istruzioni min / max impacchettate implementa una funzione di comparazione impaccata. Spiacente, non ho tempo in questo momento per cercare una pagina che ricordo di aver visto a riguardo, ma spero che la ricerca su reti di smistamento SIMD o SSE aumenti qualcosa.

SSE x86 ha istruzioni min e max con un intero intero a 32 bit per i vettori di quattro inte a 32 bit. AVX2 (Haswell e successivi) hanno lo stesso ma per vettori 256b di 8 pollici. Ci sono anche istruzioni shuffle efficienti.

Se hai molti piccoli tipi indipendenti, potrebbe essere possibile fare 4 o 8 tipi in parallelo usando i vettori. Esp. se scegli gli elementi in modo casuale (quindi i dati da ordinare non saranno contigui in memoria comunque), puoi evitare mescolamenti e semplicemente confrontare nell'ordine che ti serve. 10 registri per contenere tutti i dati da 4 (AVX2: 8) elenchi da 10 pollici lasciano ancora 6 registri per lo spazio scratch.

Le reti di ordinamento vettoriale sono meno efficienti se è necessario anche ordinare i dati associati. In tal caso, il modo più efficace sembra essere quello di utilizzare un pacchetto-confronto per ottenere una maschera di quali elementi sono cambiati e utilizzare quella maschera per fondere i vettori di (riferimenti a) dati associati.


26

Che dire di un ordinamento di selezione non srotolato e senza filiali?

#include <iostream>
#include <algorithm>
#include <random>

//return the index of the minimum element in array a
int min(const int * const a) {
  int m = a[0];
  int indx = 0;
  #define TEST(i) (m > a[i]) && (m = a[i], indx = i ); 
  //see http://stackoverflow.com/a/7074042/2140449
  TEST(1);
  TEST(2);
  TEST(3);
  TEST(4);
  TEST(5);
  TEST(6);
  TEST(7);
  TEST(8);
  TEST(9);
  #undef TEST
  return indx;
}

void sort( int * const a ){
  int work[10];
  int indx;
  #define GET(i) indx = min(a); work[i] = a[indx]; a[indx] = 2147483647; 
  //get the minimum, copy it to work and set it at max_int in a
  GET(0);
  GET(1);
  GET(2);
  GET(3);
  GET(4);
  GET(5);
  GET(6);
  GET(7);
  GET(8);
  GET(9);
  #undef GET
  #define COPY(i) a[i] = work[i];
  //copy back to a
  COPY(0);
  COPY(1);
  COPY(2);
  COPY(3);
  COPY(4);
  COPY(5);
  COPY(6);
  COPY(7);
  COPY(8);
  COPY(9);
  #undef COPY
}

int main() {
  //generating and printing a random array
  int a[10] = { 1,2,3,4,5,6,7,8,9,10 };
  std::random_device rd;
  std::mt19937 g(rd());
  std::shuffle( a, a+10, g);
  for (int i = 0; i < 10; i++) {
    std::cout << a[i] << ' ';
  }
  std::cout << std::endl;

  //sorting and printing again
  sort(a);
  for (int i = 0; i < 10; i++) {
    std::cout << a[i] << ' ';
  } 

  return 0;
}

http://coliru.stacked-crooked.com/a/71e18bc4f7fa18c6

Le uniche righe pertinenti sono le prime due #define.

Utilizza due elenchi e ricontrolla interamente il primo per dieci volte, il che sarebbe un ordinamento di selezione mal implementato, tuttavia evita rami e loop di lunghezza variabile, che possono compensare con processori moderni e un set di dati così piccolo.


Prova delle prestazioni

Ho confrontato la rete di smistamento e il mio codice sembra essere più lento. Tuttavia ho provato a rimuovere lo srotolamento e la copia. In esecuzione questo codice:

#include <iostream>
#include <algorithm>
#include <random>
#include <chrono>

int min(const int * const a, int i) {
  int m = a[i];
  int indx = i++;
  for ( ; i<10; i++) 
    //see http://stackoverflow.com/a/7074042/2140449
    (m > a[i]) && (m = a[i], indx = i ); 
  return indx;
}

void sort( int * const a ){
  for (int i = 0; i<9; i++)
    std::swap(a[i], a[min(a,i)]); //search only forward
}


void sortNet10(int * const data) {  // ten-input sorting network by Waksman, 1969
    int swap;
    if (data[0] > data[5]) { swap = data[0]; data[0] = data[5]; data[5] = swap; }
    if (data[1] > data[6]) { swap = data[1]; data[1] = data[6]; data[6] = swap; }
    if (data[2] > data[7]) { swap = data[2]; data[2] = data[7]; data[7] = swap; }
    if (data[3] > data[8]) { swap = data[3]; data[3] = data[8]; data[8] = swap; }
    if (data[4] > data[9]) { swap = data[4]; data[4] = data[9]; data[9] = swap; }
    if (data[0] > data[3]) { swap = data[0]; data[0] = data[3]; data[3] = swap; }
    if (data[5] > data[8]) { swap = data[5]; data[5] = data[8]; data[8] = swap; }
    if (data[1] > data[4]) { swap = data[1]; data[1] = data[4]; data[4] = swap; }
    if (data[6] > data[9]) { swap = data[6]; data[6] = data[9]; data[9] = swap; }
    if (data[0] > data[2]) { swap = data[0]; data[0] = data[2]; data[2] = swap; }
    if (data[3] > data[6]) { swap = data[3]; data[3] = data[6]; data[6] = swap; }
    if (data[7] > data[9]) { swap = data[7]; data[7] = data[9]; data[9] = swap; }
    if (data[0] > data[1]) { swap = data[0]; data[0] = data[1]; data[1] = swap; }
    if (data[2] > data[4]) { swap = data[2]; data[2] = data[4]; data[4] = swap; }
    if (data[5] > data[7]) { swap = data[5]; data[5] = data[7]; data[7] = swap; }
    if (data[8] > data[9]) { swap = data[8]; data[8] = data[9]; data[9] = swap; }
    if (data[1] > data[2]) { swap = data[1]; data[1] = data[2]; data[2] = swap; }
    if (data[3] > data[5]) { swap = data[3]; data[3] = data[5]; data[5] = swap; }
    if (data[4] > data[6]) { swap = data[4]; data[4] = data[6]; data[6] = swap; }
    if (data[7] > data[8]) { swap = data[7]; data[7] = data[8]; data[8] = swap; }
    if (data[1] > data[3]) { swap = data[1]; data[1] = data[3]; data[3] = swap; }
    if (data[4] > data[7]) { swap = data[4]; data[4] = data[7]; data[7] = swap; }
    if (data[2] > data[5]) { swap = data[2]; data[2] = data[5]; data[5] = swap; }
    if (data[6] > data[8]) { swap = data[6]; data[6] = data[8]; data[8] = swap; }
    if (data[2] > data[3]) { swap = data[2]; data[2] = data[3]; data[3] = swap; }
    if (data[4] > data[5]) { swap = data[4]; data[4] = data[5]; data[5] = swap; }
    if (data[6] > data[7]) { swap = data[6]; data[6] = data[7]; data[7] = swap; }
    if (data[3] > data[4]) { swap = data[3]; data[3] = data[4]; data[4] = swap; }
    if (data[5] > data[6]) { swap = data[5]; data[5] = data[6]; data[6] = swap; }
}


std::chrono::duration<double> benchmark( void(*func)(int * const), const int seed ) {
  std::mt19937 g(seed);
  int a[10] = {10,11,12,13,14,15,16,17,18,19};
  std::chrono::high_resolution_clock::time_point t1, t2; 
  t1 = std::chrono::high_resolution_clock::now();
  for (long i = 0; i < 1e7; i++) {
    std::shuffle( a, a+10, g);
    func(a);
  }
  t2 = std::chrono::high_resolution_clock::now();
  return std::chrono::duration_cast<std::chrono::duration<double>>(t2 - t1);
}

int main() {
  std::random_device rd;
  for (int i = 0; i < 10; i++) {
    const int seed = rd();
    std::cout << "seed = " << seed << std::endl;
    std::cout << "sortNet10: " << benchmark(sortNet10, seed).count() << std::endl;
    std::cout << "sort:      " << benchmark(sort,      seed).count() << std::endl;
  }
  return 0;
}

Sto ottenendo costantemente risultati migliori per l'ordinamento di selezione senza filiali rispetto alla rete di ordinamento.

$ gcc -v
gcc version 5.2.0 (GCC) 
$ g++ -std=c++11 -Ofast sort.cpp && ./a.out
seed = -1727396418
sortNet10: 2.24137
sort:      2.21828
seed = 2003959850
sortNet10: 2.23914
sort:      2.21641
seed = 1994540383
sortNet10: 2.23782
sort:      2.21778
seed = 1258259982
sortNet10: 2.25199
sort:      2.21801
seed = 1821086932
sortNet10: 2.25535
sort:      2.2173
seed = 412262735
sortNet10: 2.24489
sort:      2.21776
seed = 1059795817
sortNet10: 2.29226
sort:      2.21777
seed = -188551272
sortNet10: 2.23803
sort:      2.22996
seed = 1043757247
sortNet10: 2.2503
sort:      2.23604
seed = -268332483
sortNet10: 2.24455
sort:      2.24304

4
I risultati non sono molto impressionanti, ma in realtà quello che mi sarei aspettato. La rete di smistamento minimizza i confronti, non gli swap. Quando tutti i valori sono già nella cache, i confronti sono molto più economici degli swap, quindi un ordinamento di selezione (che minimizza il numero di swap) ha il sopravvento. (e non ci sono molti altri confronti: rete con 29 compasioni, fino a 29 swap ?; vs. ordinamento di selezione con 45 confronti e al massimo 9 swap)
esempio

7
Oh e ha dei rami - a meno che la linea non for ( ; i<10; i++) (m > a[i]) && (m = a[i], indx = i ); sia eccezionalmente ben ottimizzata. (il corto circuito di solito è una forma di ramificazione)
esempio

1
@EugeneRyabtsev anche quello, ma è alimentato esattamente con le stesse sequenze casuali tutte le volte, quindi dovrebbe annullarsi. Ho provato a cambiare std::shufflecon for (int n = 0; n<10; n++) a[n]=g();. Il tempo di esecuzione è dimezzato e la rete ora è più veloce.
DarioP,

Come si confronta questo con libc ++ std::sort?
gnzlbg

1
@gnzlbg Ho provato anche io std::sort, ma stava andando così male che non l'ho nemmeno incluso nel benchmark. Immagino che con piccoli set di dati ci sia abbastanza sovraccarico.
DarioP

20

La domanda non dice che si tratti di una specie di applicazione basata sul web. L'unica cosa che ha attirato la mia attenzione è stata:

Sto campionando un insieme di dati di miliardi di elementi e ogni volta ho bisogno di selezionarne 10 numeri (semplificati) e ordinarli (e trarre conclusioni dall'elenco dei 10 elementi ordinati).

Come ingegnere del software e dell'hardware questo mi urla assolutamente "FPGA" . Non so che tipo di conclusioni devi trarre dalla serie ordinata di numeri o da dove provengano i dati, ma so che sarebbe quasi banale elaborare da qualche parte tra cento milioni e un miliardo di questi "sort-and- analizzare "operazioni al secondo . In passato ho svolto attività di sequenziamento del DNA assistito da FPGA. È quasi impossibile battere l'enorme potenza di elaborazione degli FPGA quando il problema è adatto per quel tipo di soluzione.

Ad un certo livello l'unico fattore limitante diventa la velocità con cui è possibile trasferire i dati in un FPGA e la velocità con cui è possibile estrarli.

Come punto di riferimento, ho progettato un processore di immagini in tempo reale ad alte prestazioni che ha ricevuto dati di immagine RGB a 32 bit a una velocità di circa 300 milioni di pixel al secondo. I dati sono stati trasmessi in streaming attraverso i filtri FIR, i moltiplicatori di matrici, le tabelle di ricerca, i blocchi di rilevamento dei bordi spaziali e una serie di altre operazioni prima di uscire dall'altra estremità. Tutto questo su un FPGA Xilinx Virtex2 relativamente piccolo con clock interno che va da circa 33 MHz a, se non ricordo male, da 400 MHz. Oh, sì, aveva anche un'implementazione del controller DDR2 e gestiva due banchi di memoria DDR2.

Un FPGA può emettere una sorta di dieci numeri a 32 bit su ogni transizione di clock mentre funziona a centinaia di MHz. Ci sarebbe un breve ritardo all'inizio dell'operazione poiché i dati riempiono la pipeline di elaborazione. Dopodiché dovresti essere in grado di ottenere un risultato per orologio. O più se l'elaborazione può essere parallelizzata replicando la pipeline di ordinamento e analisi. La soluzione, in linea di principio, è quasi banale.

Il punto è: se l'applicazione non è associata a PC e il flusso e l'elaborazione dei dati sono "compatibili" con una soluzione FPGA (stand-alone o come scheda coprocessore nella macchina) non c'è modo di andare essere in grado di battere il livello raggiungibile di prestazioni con software scritto in qualsiasi lingua, indipendentemente dall'algoritmo.

MODIFICARE:

Ho appena eseguito una ricerca rapida e ho trovato un documento che potrebbe esserti utile. Sembra che risale al 2012. Puoi fare MOLTO meglio nelle prestazioni oggi (e anche allora). Ecco qui:

Ordinamento di reti su FPGA


10

Di recente ho scritto una piccola classe che utilizza l'algoritmo Bose-Nelson per generare una rete di ordinamento in fase di compilazione.

Può essere utilizzato per creare un ordinamento molto veloce per 10 numeri.

/**
 * A Functor class to create a sort for fixed sized arrays/containers with a
 * compile time generated Bose-Nelson sorting network.
 * \tparam NumElements  The number of elements in the array or container to sort.
 * \tparam T            The element type.
 * \tparam Compare      A comparator functor class that returns true if lhs < rhs.
 */
template <unsigned NumElements, class Compare = void> class StaticSort
{
    template <class A, class C> struct Swap
    {
        template <class T> inline void s(T &v0, T &v1)
        {
            T t = Compare()(v0, v1) ? v0 : v1; // Min
            v1 = Compare()(v0, v1) ? v1 : v0; // Max
            v0 = t;
        }

        inline Swap(A &a, const int &i0, const int &i1) { s(a[i0], a[i1]); }
    };

    template <class A> struct Swap <A, void>
    {
        template <class T> inline void s(T &v0, T &v1)
        {
            // Explicitly code out the Min and Max to nudge the compiler
            // to generate branchless code.
            T t = v0 < v1 ? v0 : v1; // Min
            v1 = v0 < v1 ? v1 : v0; // Max
            v0 = t;
        }

        inline Swap(A &a, const int &i0, const int &i1) { s(a[i0], a[i1]); }
    };

    template <class A, class C, int I, int J, int X, int Y> struct PB
    {
        inline PB(A &a)
        {
            enum { L = X >> 1, M = (X & 1 ? Y : Y + 1) >> 1, IAddL = I + L, XSubL = X - L };
            PB<A, C, I, J, L, M> p0(a);
            PB<A, C, IAddL, J + M, XSubL, Y - M> p1(a);
            PB<A, C, IAddL, J, XSubL, M> p2(a);
        }
    };

    template <class A, class C, int I, int J> struct PB <A, C, I, J, 1, 1>
    {
        inline PB(A &a) { Swap<A, C> s(a, I - 1, J - 1); }
    };

    template <class A, class C, int I, int J> struct PB <A, C, I, J, 1, 2>
    {
        inline PB(A &a) { Swap<A, C> s0(a, I - 1, J); Swap<A, C> s1(a, I - 1, J - 1); }
    };

    template <class A, class C, int I, int J> struct PB <A, C, I, J, 2, 1>
    {
        inline PB(A &a) { Swap<A, C> s0(a, I - 1, J - 1); Swap<A, C> s1(a, I, J - 1); }
    };

    template <class A, class C, int I, int M, bool Stop = false> struct PS
    {
        inline PS(A &a)
        {
            enum { L = M >> 1, IAddL = I + L, MSubL = M - L};
            PS<A, C, I, L, (L <= 1)> ps0(a);
            PS<A, C, IAddL, MSubL, (MSubL <= 1)> ps1(a);
            PB<A, C, I, IAddL, L, MSubL> pb(a);
        }
    };

    template <class A, class C, int I, int M> struct PS <A, C, I, M, true>
    {
        inline PS(A &a) {}
    };

public:
    /**
     * Sorts the array/container arr.
     * \param  arr  The array/container to be sorted.
     */
    template <class Container> inline void operator() (Container &arr) const
    {
        PS<Container, Compare, 1, NumElements, (NumElements <= 1)> ps(arr);
    };

    /**
     * Sorts the array arr.
     * \param  arr  The array to be sorted.
     */
    template <class T> inline void operator() (T *arr) const
    {
        PS<T*, Compare, 1, NumElements, (NumElements <= 1)> ps(arr);
    };
};

#include <iostream>
#include <vector>

int main(int argc, const char * argv[])
{
    enum { NumValues = 10 };

    // Arrays
    {
        int rands[NumValues];
        for (int i = 0; i < NumValues; ++i) rands[i] = rand() % 100;
        std::cout << "Before Sort: \t";
        for (int i = 0; i < NumValues; ++i) std::cout << rands[i] << " ";
        std::cout << "\n";
        StaticSort<NumValues> staticSort;
        staticSort(rands);
        std::cout << "After Sort: \t";
        for (int i = 0; i < NumValues; ++i) std::cout << rands[i] << " ";
        std::cout << "\n";
    }

    std::cout << "\n";

    // STL Vector
    {
        std::vector<int> rands(NumValues);
        for (int i = 0; i < NumValues; ++i) rands[i] = rand() % 100;
        std::cout << "Before Sort: \t";
        for (int i = 0; i < NumValues; ++i) std::cout << rands[i] << " ";
        std::cout << "\n";
        StaticSort<NumValues> staticSort;
        staticSort(rands);
        std::cout << "After Sort: \t";
        for (int i = 0; i < NumValues; ++i) std::cout << rands[i] << " ";
        std::cout << "\n";
    }

    return 0;
}

Si noti che invece di if (compare) swapun'istruzione, codifichiamo esplicitamente gli operatori ternari per min e max. Questo per aiutare a spingere il compilatore ad usare il codice branchless.

Punti di riferimenti

I seguenti benchmark sono stati compilati con clang -O3 e sono stati pubblicati sul mio macbook air di metà 2012.

Ordinamento di dati casuali

Confrontandolo con il codice DarioP, ecco il numero di millisecondi necessari per ordinare 1 milione di array int a 32 bit della dimensione 10:

Ordinamento netto 10: 88.774 ms Ordinato
Bose-Nelson ordinamento 10: 27.815 ms

Utilizzando questo approccio basato su modelli, possiamo anche generare reti di ordinamento al momento della compilazione per altri numeri di elementi.

Tempo (in millisecondi) per ordinare 1 milione di array di varie dimensioni.
Il numero di millisecondi per gli array di dimensioni 2, 4, 8 è rispettivamente 1.943, 8.655, 20.246.
Tempi di ordinamento statico Bose-Nelson templated C ++

Crediti a Glenn Teitelbaum per l'ordinamento di inserzione non srotolato.

Ecco gli orologi medi per ordinamento per piccoli array di 6 elementi. Il codice di riferimento e gli esempi sono disponibili a questa domanda: il
tipo più veloce di array a 6 int di lunghezza fissa

Direct call to qsort library function       : 326.81
Naive implementation (insertion sort)       : 132.98
Insertion Sort (Daniel Stutzbach)           : 104.04
Insertion Sort Unrolled                     : 99.64
Insertion Sort Unrolled (Glenn Teitelbaum)  : 81.55
Rank Order                                  : 44.01
Rank Order with registers                   : 42.40
Sorting Networks (Daniel Stutzbach)         : 88.06
Sorting Networks (Paul R)                   : 31.64
Sorting Networks 12 with Fast Swap          : 29.68
Sorting Networks 12 reordered Swap          : 28.61
Reordered Sorting Network w/ fast swap      : 24.63
Templated Sorting Network (this class)      : 25.37

Si comporta velocemente come l'esempio più veloce nella domanda per 6 elementi.

Prestazioni per l'ordinamento dei dati ordinati

Spesso, le matrici di input possono essere già ordinate o per lo più ordinate.
In tali casi, l'ordinamento per inserzione può essere una scelta migliore.

inserisci qui la descrizione dell'immagine

Potresti voler scegliere un algoritmo di ordinamento appropriato in base ai dati.

Il codice utilizzato per i parametri di riferimento è disponibile qui .


Hai qualche possibilità di aggiungere un confronto per il mio algo di seguito?
Glenn Teitelbaum,

@GlennTeitelbaum hai qualche possibilità di aggiungerlo ai tuoi parametri di riferimento e di divulgare mezzi e risultati?
Greybeard,

Complimenti per l'aggiunta di dati sull'ordinamento dell'input ordinato.
Greybeard,

Su alcuni sistemi v1 = v0 < v1 ? v1 : v0; // Maxpuò ancora succursarsi, in quel caso può essere sostituito con v1 += v0 - tif tis v0then v1 + v0 -t == v1 + v0 - v0 == v1else tis v1ev1 + v0 -t == v1 + v0 - v1 == v0
Glenn Teitelbaum

Il ternario di solito si compila in una maxsso minssistruzione su compilatori moderni. Ma nei casi in cui non funziona, è possibile utilizzare altri modi di scambio. :)
Vettorizzato

5

Sebbene un ordinamento di rete abbia buone probabilità di essere veloce su array di piccole dimensioni, a volte non è possibile battere l'ordinamento di inserzione se ottimizzato correttamente. Ad esempio inserto batch con 2 elementi:

{
    final int a=in[0]<in[1]?in[0]:in[1];
    final int b=in[0]<in[1]?in[1]:in[0];
    in[0]=a;
    in[1]=b;
}
for(int x=2;x<10;x+=2)
{
    final int a=in[x]<in[x+1]?in[x]:in[x+1];
    final int b=in[x]<in[x+1]?in[x+1]:in[x];
    int y= x-1;

    while(y>=0&&in[y]>b)
    {
        in[y+2]= in[y];
        --y;
    }
    in[y+2]=b;
    while(y>=0&&in[y]>a)
    {
        in[y+1]= in[y];
        --y;
    }
    in[y+1]=a;
}

Non sei sicuro del motivo per cui ripeti in[y+2]= in[y];, errore di battitura?
Glenn Teitelbaum,

Wow, come ho fatto? E come ci è voluto tanto tempo perché qualcuno se ne accorgesse? La risposta: non è un refuso: stavo adattando un diverso algoritmo che aveva sia una chiave che una matrice di valori.
Warren,

3

Puoi srotolarti completamente insertion sort

Per rendere ciò più semplice, templateè possibile utilizzare s ricorsivi senza sovraccarico di funzioni. Poiché è già un template, intpuò essere anche un templateparametro. Questo rende anche la creazione di dimensioni di array di codifica diverse da 10.

Si noti che l'ordinamento int x[10]della chiamata è insert_sort<int, 9>::sort(x);poiché la classe utilizza l'indice dell'ultimo elemento. Questo potrebbe essere racchiuso, ma sarebbe più codice da leggere.

template <class T, int NUM>
class insert_sort;

template <class T>
class insert_sort<T,0>
// stop template recursion
// sorting 1 item is a no-op
{
public:
    static void place(T *x) {}
    static void sort(T * x) {}
};

template <class T, int NUM>
class insert_sort
// use template recursion to do insertion sort
// NUM is the index of the last item, eg. for x[10] call <9>
{
public:
    static void place(T *x)
    {
        T t1=x[NUM-1];
        T t2=x[NUM];
        if (t1 > t2)
        {
            x[NUM-1]=t2;
            x[NUM]=t1;
            insert_sort<T,NUM-1>::place(x);
        }
    }
    static void sort(T * x)
    {
        insert_sort<T,NUM-1>::sort(x); // sort everything before
        place(x);                    // put this item in
    }
};

Nel mio test questo è stato più veloce degli esempi di rete di smistamento.


0

Per motivi analoghi a quelli che ho descritto qui , le seguenti funzioni di ordinamento, sort6_iterator()e sort10_iterator_local(), dovrebbe funzionare bene, in cui la rete di smistamento è stato preso da qui :

template<class IterType> 
inline void sort10_iterator(IterType it) 
{
#define SORT2(x,y) {if(data##x>data##y)std::swap(data##x,data##y);}
#define DD1(a)   auto data##a=*(data+a);
#define DD2(a,b) auto data##a=*(data+a), data##b=*(data+b);
#define CB1(a)   *(data+a)=data##a;
#define CB2(a,b) *(data+a)=data##a;*(data+b)=data##b;
  DD2(1,4) SORT2(1,4) DD2(7,8) SORT2(7,8) DD2(2,3) SORT2(2,3) DD2(5,6) SORT2(5,6) DD2(0,9) SORT2(0,9) 
  SORT2(2,5) SORT2(0,7) SORT2(8,9) SORT2(3,6) 
  SORT2(4,9) SORT2(0,1) 
  SORT2(0,2) CB1(0) SORT2(6,9) CB1(9) SORT2(3,5) SORT2(4,7) SORT2(1,8) 
  SORT2(3,4) SORT2(5,8) SORT2(6,7) SORT2(1,2) 
  SORT2(7,8) CB1(8) SORT2(1,3) CB1(1) SORT2(2,5) SORT2(4,6) 
  SORT2(2,3) CB1(2) SORT2(6,7) CB1(7) SORT2(4,5) 
  SORT2(3,4) CB2(3,4) SORT2(5,6) CB2(5,6) 
#undef CB1
#undef CB2
#undef DD1
#undef DD2
#undef SORT2
}

Per chiamare questa funzione ho passato un std::vectoriteratore.


0

Un ordinamento per inserzione richiede in media 29,6 confronti per ordinare 10 ingressi con il caso migliore di 9 e il peggiore di 45 (dato l'input che è in ordine inverso).

Uno shellsort {9,6,1} richiederà in media 25,5 confronti per ordinare 10 input. Il caso migliore è 14 confronti, il peggiore è 34 e l'ordinamento di un ingresso invertito richiede 22.

Pertanto, l'utilizzo di shellsort anziché di ordinamento per inserzione riduce il caso medio del 14%. Sebbene il caso migliore sia aumentato del 56%, il caso peggiore è ridotto del 24%, il che è significativo nelle applicazioni in cui è importante tenere sotto controllo le prestazioni del caso peggiore. La cassa inversa è ridotta del 51%.

Dal momento che sembri avere familiarità con l'ordinamento di inserzione, puoi implementare l'algoritmo come una rete di ordinamento per {9,6} e quindi applicare l'ordinamento di inserzione ({1}) in seguito:

i[0] with i[9]    // {9}

i[0] with i[6]    // {6}
i[1] with i[7]    // {6}
i[2] with i[8]    // {6}
i[3] with i[9]    // {6}

i[0 ... 9]        // insertion sort
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.