Ordinamento più rapido di array a 6 int di lunghezza fissa


401

Risposta a un'altra domanda Stack Overflow ( questa ) mi sono imbattuto in un interessante sotto-problema. Qual è il modo più veloce per ordinare un array di 6 numeri interi?

Poiché la domanda è di livello molto basso:

  • non possiamo supporre che le librerie siano disponibili (e la chiamata stessa abbia il suo costo), solo C semplice
  • per evitare di svuotare la pipeline di istruzioni (che ha un costo molto elevato) dovremmo probabilmente ridurre al minimo rami, salti e ogni altro tipo di interruzione del flusso di controllo (come quelli nascosti dietro punti di sequenza in &&o ||).
  • lo spazio è limitato e minimizzare i registri e l'uso della memoria è un problema, idealmente nella posizione l'ordinamento è probabilmente il migliore.

In realtà questa domanda è una specie di golf in cui l'obiettivo non è minimizzare la lunghezza della sorgente ma i tempi di esecuzione. Lo chiamo codice 'Zening' come usato nel titolo del libro Zen of Code ottimizzazione di Michael Abrash e dei suoi sequel .

Per quanto riguarda il motivo per cui è interessante, ci sono diversi livelli:

  • l'esempio è semplice e facile da capire e misurare, non molta abilità in C coinvolta
  • mostra gli effetti della scelta di un buon algoritmo per il problema, ma anche gli effetti del compilatore e dell'hardware sottostante.

Ecco la mia implementazione di riferimento (ingenua, non ottimizzata) e il mio set di test.

#include <stdio.h>

static __inline__ int sort6(int * d){

    char j, i, imin;
    int tmp;
    for (j = 0 ; j < 5 ; j++){
        imin = j;
        for (i = j + 1; i < 6 ; i++){
            if (d[i] < d[imin]){
                imin = i;
            }
        }
        tmp = d[j];
        d[j] = d[imin];
        d[imin] = tmp;
    }
}

static __inline__ unsigned long long rdtsc(void)
{
  unsigned long long int x;
     __asm__ volatile (".byte 0x0f, 0x31" : "=A" (x));
     return x;
}

int main(int argc, char ** argv){
    int i;
    int d[6][5] = {
        {1, 2, 3, 4, 5, 6},
        {6, 5, 4, 3, 2, 1},
        {100, 2, 300, 4, 500, 6},
        {100, 2, 3, 4, 500, 6},
        {1, 200, 3, 4, 5, 600},
        {1, 1, 2, 1, 2, 1}
    };

    unsigned long long cycles = rdtsc();
    for (i = 0; i < 6 ; i++){
        sort6(d[i]);
        /*
         * printf("d%d : %d %d %d %d %d %d\n", i,
         *  d[i][0], d[i][6], d[i][7],
         *  d[i][8], d[i][9], d[i][10]);
        */
    }
    cycles = rdtsc() - cycles;
    printf("Time is %d\n", (unsigned)cycles);
}

Risultati grezzi

Man mano che il numero di varianti sta diventando grande, le ho raccolte tutte in una suite di test che può essere trovata qui . I test effettivamente utilizzati sono un po 'meno ingenui di quelli mostrati sopra, grazie a Kevin Stock. È possibile compilare ed eseguirlo nel proprio ambiente. Sono abbastanza interessato dal comportamento su differenti architetture / compilatori target. (OK ragazzi, mettetelo nelle risposte, farò +1 a tutti i contributori di un nuovo set di risultati).

Ho dato la risposta a Daniel Stutzbach (per il golf) un anno fa, poiché era alla fonte della soluzione più veloce in quel momento (reti di smistamento).

Linux 64 bit, gcc 4.6.1 64 bit, Intel Core 2 Duo E8400, -O2

  • Chiamata diretta alla funzione di libreria qsort: 689.38
  • Implementazione ingenua (ordinamento per inserzione): 285.70
  • Inserimento ordinamento (Daniel Stutzbach): 142.12
  • Inserimento ordinamento non srotolato: 125,47
  • Ordine di rango: 102.26
  • Rango con registri: 58.03
  • Reti di smistamento (Daniel Stutzbach): 111.68
  • Reti di ordinamento (Paul R): 66.36
  • Reti di ordinamento 12 con Fast Swap: 58.86
  • Scambio riordinato Reti ordinate 12: 53.74
  • Ordinamento reti 12 riordinato Simple Swap: 31.54
  • Rete di smistamento riordinata con scambio rapido: 31.54
  • Rete di smistamento riordinata con scambio rapido V2: 33.63
  • Inbound Bubble Sort (Paolo Bonzini): 48,85
  • Ordinamento di inserzione non srotolato (Paolo Bonzini): 75.30

Linux 64 bit, gcc 4.6.1 64 bit, Intel Core 2 Duo E8400, -O1

  • Chiamata diretta alla funzione di libreria qsort: 705.93
  • Implementazione ingenua (tipo inserzione): 135.60
  • Inserimento ordinamento (Daniel Stutzbach): 142.11
  • Inserimento ordinamento non srotolato: 126,75
  • Ordine di grado: 46.42
  • Ordine dei ranghi con i registri: 43.58
  • Reti di smistamento (Daniel Stutzbach): 115.57
  • Reti di ordinamento (Paul R): 64.44
  • Reti di ordinamento 12 con Fast Swap: 61.98
  • Scambio riordinato per reti di smistamento 12: 54.67
  • Ordinamento reti 12 riordinato Simple Swap: 31.54
  • Rete di smistamento riordinata con scambio rapido: 31.24
  • Rete di smistamento riordinata con scambio rapido V2: 33.07
  • Inbound Bubble Sort (Paolo Bonzini): 45,79
  • Ordinamento di inserzione non srotolato (Paolo Bonzini): 80.15

Ho incluso entrambi i risultati -O1 e -O2 perché sorprendentemente per diversi programmi O2 è meno efficiente di O1. Mi chiedo quale ottimizzazione specifica abbia questo effetto?

Commenti sulle soluzioni proposte

Inserimento ordinamento (Daniel Stutzbach)

Come previsto, minimizzare le filiali è davvero una buona idea.

Reti di smistamento (Daniel Stutzbach)

Meglio dell'ordinamento per inserzione. Mi chiedevo se l'effetto principale non fosse derivato dall'evitare il loop esterno. Ho provato con l'ordinamento di inserzione non srotolato per verificare e in effetti otteniamo all'incirca le stesse cifre (il codice è qui ).

Reti di ordinamento (Paul R)

Il migliore finora. Il codice effettivo che ho usato per testare è qui . Non so ancora perché sia ​​quasi due volte più veloce dell'altra implementazione della rete di smistamento. Il passaggio dei parametri? Max veloce?

Ordinamento delle reti 12 SWAP con Fast Swap

Come suggerito da Daniel Stutzbach, ho combinato la sua rete di smistamento a 12 scambi con lo scambio veloce senza rami (il codice è qui ). È davvero più veloce, il migliore finora con un piccolo margine (circa il 5%) come ci si potrebbe aspettare usando 1 swap in meno.

È anche interessante notare che lo swap senza rami sembra essere molto (4 volte) meno efficiente di quello semplice che utilizza se su architettura PPC.

Chiamata alla libreria qsort

Per dare un altro punto di riferimento ho anche provato, come suggerito, a chiamare semplicemente la libreria qsort (il codice è qui ). Come previsto, è molto più lento: da 10 a 30 volte più lento ... come è diventato evidente con la nuova suite di test, il problema principale sembra essere il carico iniziale della libreria dopo la prima chiamata, e non si confronta così male con gli altri versione. È solo tra 3 e 20 volte più lento sul mio Linux. Su alcune architetture utilizzate per i test di altri sembra addirittura più veloce (ne sono davvero sorpreso, dato che la libreria qsort utilizza un'API più complessa).

Ordine di rango

Rex Kerr ha proposto un altro metodo completamente diverso: per ogni elemento dell'array calcolare direttamente la sua posizione finale. Ciò è efficiente perché l'ordine di classificazione non necessita di diramazioni. Lo svantaggio di questo metodo è che richiede tre volte la quantità di memoria dell'array (una copia dell'array e delle variabili per memorizzare gli ordini di rango). I risultati delle prestazioni sono molto sorprendenti (e interessanti). Sulla mia architettura di riferimento con sistema operativo a 32 bit e Intel Core2 Quad E8300, il conteggio dei cicli era leggermente inferiore a 1000 (come le reti di ordinamento con swap branching). Ma quando compilato ed eseguito sul mio box a 64 bit (Intel Core2 Duo) ha funzionato molto meglio: è diventato il più veloce finora. Alla fine ho scoperto il vero motivo. La mia scatola a 32 bit usa gcc 4.4.1 e la mia scatola a 64 bit gcc 4.4.

aggiornamento :

Come mostrato dai dati pubblicati sopra, questo effetto è stato ancora migliorato dalle versioni successive di gcc e il Rank Order è diventato costantemente due volte più veloce di qualsiasi altra alternativa.

Ordinamento di reti 12 con swap riordinato

La straordinaria efficienza della proposta Rex Kerr con gcc 4.4.3 mi ha fatto meravigliare: come può un programma con un utilizzo della memoria 3 volte maggiore essere più veloce delle reti di smistamento senza rami? La mia ipotesi era che avesse meno dipendenze del tipo letto dopo scrittura, consentendo un migliore utilizzo del programmatore di istruzioni superscalare di x86. Questo mi ha dato un'idea: riordinare gli swap per ridurre al minimo le dipendenze di lettura dopo scrittura. In parole povere: quando lo fai SWAP(1, 2); SWAP(0, 2);devi aspettare che il primo scambio sia terminato prima di eseguire il secondo perché entrambi accedono a una cella di memoria comune. Quando lo fai SWAP(1, 2); SWAP(4, 5);il processore può eseguire entrambi in parallelo. L'ho provato e funziona come previsto, le reti di smistamento funzionano circa il 10% più velocemente.

Ordinamento delle reti 12 con Simple Swap

Un anno dopo il post originale suggerito da Steinar H. Gunderson, non dovremmo cercare di superare in astuzia il compilatore e mantenere semplice il codice di scambio. È davvero una buona idea in quanto il codice risultante è circa il 40% più veloce! Ha anche proposto uno swap ottimizzato a mano utilizzando il codice assembly in linea x86 che può ancora risparmiare qualche altro ciclo. Il più sorprendente (dice volumi sulla psicologia del programmatore) è che un anno fa nessuno ha usato quella versione di swap. Il codice che ho usato per testare è qui . Altri hanno suggerito altri modi per scrivere uno scambio C veloce, ma produce le stesse prestazioni di quello semplice con un compilatore decente.

Il codice "migliore" è ora il seguente:

static inline void sort6_sorting_network_simple_swap(int * d){
#define min(x, y) (x<y?x:y)
#define max(x, y) (x<y?y:x) 
#define SWAP(x,y) { const int a = min(d[x], d[y]); \
                    const int b = max(d[x], d[y]); \
                    d[x] = a; d[y] = b; }
    SWAP(1, 2);
    SWAP(4, 5);
    SWAP(0, 2);
    SWAP(3, 5);
    SWAP(0, 1);
    SWAP(3, 4);
    SWAP(1, 4);
    SWAP(0, 3);
    SWAP(2, 5);
    SWAP(1, 3);
    SWAP(2, 4);
    SWAP(2, 3);
#undef SWAP
#undef min
#undef max
}

Se crediamo che il nostro set di test (e, sì, è piuttosto scarso, il suo semplice vantaggio è essere breve, semplice e facile da capire cosa stiamo misurando), il numero medio di cicli del codice risultante per un ordinamento è inferiore a 40 cicli ( Vengono eseguiti 6 test). Ciò significa che ogni scambio è in media di 4 cicli. Lo chiamo incredibilmente veloce. Qualche altro miglioramento possibile?


2
Hai dei vincoli sugli ints? Ad esempio, possiamo supporre che per qualsiasi 2 x, y x-ye x+ynon causi underflow o overflow?
Matthieu M.

3
Dovresti provare a combinare la mia rete di ordinamento a 12 scambi con la funzione di scambio senza rami di Paul. La sua soluzione passa tutti i parametri come elementi separati nello stack anziché un singolo puntatore a un array. Ciò potrebbe anche fare la differenza.
Daniel Stutzbach,

2
Si noti che la corretta implementazione di rdtsc su 64 bit è __asm__ volatile (".byte 0x0f, 0x31; shlq $32, %%rdx; orq %%rdx, %0" : "=a" (x) : : "rdx");dovuta al fatto che rdtsc inserisce la risposta in EDX: EAX mentre GCC lo prevede in un unico registro a 64 bit. Puoi vedere il bug compilando in -O3. Vedi anche sotto il mio commento a Paul R su uno SWAP più veloce.
Paolo Bonzini,

3
@Tyler: come lo implementate a livello di assembly senza un ramo?
Loren Pechtel,

4
@Loren: CMP EAX, EBX; SBB EAX, EAXinserirà 0 o 0xFFFFFFFF a EAXseconda che EAXsia maggiore o minore di EBX, rispettivamente. SBBè "sottrai con prestito", la controparte di ADC("aggiungi con carry"); il bit di stato a cui si fa riferimento è il bit di riporto. Poi di nuovo, me lo ricordo ADCe ho SBBavuto una terribile latenza e throughput sul Pentium 4 vs. ADDe SUB, ed erano ancora due volte più lenti delle CPU Core. Dall'80386 ci sono anche istruzioni per l' SETccarchiviazione CMOVcccondizionale e lo spostamento condizionale, ma sono anche lente.
j_random_hacker il

Risposte:


162

Per qualsiasi ottimizzazione, è sempre meglio testare, testare, testare. Proverei almeno a ordinare le reti e le inserzioni. Se scommettessi, metterei i miei soldi in ordine di inserzione in base all'esperienza passata.

Sai qualcosa sui dati di input? Alcuni algoritmi funzioneranno meglio con determinati tipi di dati. Ad esempio, l'ordinamento per inserzione funziona meglio su dati ordinati o quasi ordinati, quindi sarà la scelta migliore se esiste una probabilità superiore alla media di dati quasi ordinati.

L'algoritmo che hai pubblicato è simile a un ordinamento di inserzione, ma sembra che tu abbia minimizzato il numero di swap a scapito di più confronti. I confronti sono molto più costosi degli swap, tuttavia, poiché i rami possono causare l'arresto della pipeline delle istruzioni.

Ecco un'implementazione dell'ordinamento per inserzione:

static __inline__ int sort6(int *d){
        int i, j;
        for (i = 1; i < 6; i++) {
                int tmp = d[i];
                for (j = i; j >= 1 && tmp < d[j-1]; j--)
                        d[j] = d[j-1];
                d[j] = tmp;
        }
}

Ecco come costruirò una rete di smistamento. Innanzitutto, utilizzare questo sito per generare un set minimo di macro SWAP per una rete della lunghezza appropriata. Concludendolo in una funzione mi dà:

static __inline__ int sort6(int * d){
#define SWAP(x,y) if (d[y] < d[x]) { int tmp = d[x]; d[x] = d[y]; d[y] = tmp; }
    SWAP(1, 2);
    SWAP(0, 2);
    SWAP(0, 1);
    SWAP(4, 5);
    SWAP(3, 5);
    SWAP(3, 4);
    SWAP(0, 3);
    SWAP(1, 4);
    SWAP(2, 5);
    SWAP(2, 4);
    SWAP(1, 3);
    SWAP(2, 3);
#undef SWAP
}

9
+1: bello, l'hai fatto con 12 scambi anziché i 13 nella mia rete codificata a mano e derivata empiricamente sopra. Ti darei un altro +1 se potessi per il link al sito che genera reti per te - ora aggiunto ai segnalibri.
Paul R,

9
Questa è un'idea fantastica per una funzione di ordinamento per scopi generici se si prevede che la maggior parte delle richieste siano array di piccole dimensioni. Utilizzare un'istruzione switch per i casi che si desidera ottimizzare, usando questa procedura; lasciare che il caso predefinito utilizzi una funzione di ordinamento della libreria.
Mark Ransom,

5
@Mark Una buona funzione di ordinamento delle librerie avrà già un percorso rapido per piccoli array. Molte librerie moderne useranno un QuickSort o MergeSort ricorsivo che passerà a InsertionSort dopo aver ripetuto fino a n < SMALL_CONSTANT.
Daniel Stutzbach,

3
@Mark Bene, una funzione di ordinamento della libreria C richiede che tu specifichi l'operazione di confronto tramite un porter di funzioni. Il sovraccarico di chiamare una funzione per ogni confronto è enorme. Di solito, questo è ancora il modo più pulito di andare, perché raramente questo è un percorso critico nel programma. Tuttavia, se è il percorso critico, possiamo davvero ordinare molto più velocemente se sappiamo che stiamo ordinando numeri interi e esattamente 6 di essi. :)
Daniel Stutzbach

7
@tgwh: lo scambio XOR è quasi sempre una cattiva idea.
Paul R,

63

Ecco un'implementazione che utilizza le reti di ordinamento :

inline void Sort2(int *p0, int *p1)
{
    const int temp = min(*p0, *p1);
    *p1 = max(*p0, *p1);
    *p0 = temp;
}

inline void Sort3(int *p0, int *p1, int *p2)
{
    Sort2(p0, p1);
    Sort2(p1, p2);
    Sort2(p0, p1);
}

inline void Sort4(int *p0, int *p1, int *p2, int *p3)
{
    Sort2(p0, p1);
    Sort2(p2, p3);
    Sort2(p0, p2);  
    Sort2(p1, p3);  
    Sort2(p1, p2);  
}

inline void Sort6(int *p0, int *p1, int *p2, int *p3, int *p4, int *p5)
{
    Sort3(p0, p1, p2);
    Sort3(p3, p4, p5);
    Sort2(p0, p3);  
    Sort2(p2, p5);  
    Sort4(p1, p2, p3, p4);  
}

Hai davvero bisogno di branchless mine maximplementazioni molto efficienti per questo, dal momento che questo è effettivamente ciò a cui questo codice si riduce: una sequenza di mine maxoperazioni (13 di ciascuna, in totale). Lascio questo come un esercizio per il lettore.

Si noti che questa implementazione si presta facilmente alla vettorializzazione (ad es. SIMD - la maggior parte degli ISA SIMD hanno istruzioni vettoriali min / max) e anche alle implementazioni della GPU (ad es. CUDA - essendo senza rami non ci sono problemi con la divergenza dell'ordito ecc.).

Vedi anche: Implementazione veloce dell'algoritmo per ordinare un elenco molto piccolo



1
@Paul: nel vero contesto d'uso CUDA, è sicuramente la risposta migliore. Verificherò se è (e quanto) anche nel contesto golf x64 e pubblicherò il risultato.
Kriss,

1
Sort3sarebbe più veloce (sulla maggior parte delle architetture, comunque) se notassi che (a+b+c)-(min+max)è il numero centrale.
Rex Kerr,

1
@Rex: vedo - sembra buono. Per architetture SIMD come AltiVec e SSE sarebbe lo stesso numero di cicli di istruzione (max e min sono istruzioni a ciclo singolo come aggiungi / sottragga), ma per una normale CPU scalare il tuo metodo sembra migliore.
Paul R,

2
Se lascio GCC ottimizzare min con istruzioni di movimento condizionali ottengo un aumento di velocità del 33%: #define SWAP(x,y) { int dx = d[x], dy = d[y], tmp; tmp = d[x] = dx < dy ? dx : dy; d[y] ^= dx ^ tmp; }. Qui non sto usando?: Per d [y] perché offre prestazioni leggermente peggiori, ma è quasi nel rumore.
Paolo Bonzini,

45

Poiché questi sono numeri interi e i confronti sono rapidi, perché non calcolare direttamente l'ordine di classificazione di ciascuno:

inline void sort6(int *d) {
  int e[6];
  memcpy(e,d,6*sizeof(int));
  int o0 = (d[0]>d[1])+(d[0]>d[2])+(d[0]>d[3])+(d[0]>d[4])+(d[0]>d[5]);
  int o1 = (d[1]>=d[0])+(d[1]>d[2])+(d[1]>d[3])+(d[1]>d[4])+(d[1]>d[5]);
  int o2 = (d[2]>=d[0])+(d[2]>=d[1])+(d[2]>d[3])+(d[2]>d[4])+(d[2]>d[5]);
  int o3 = (d[3]>=d[0])+(d[3]>=d[1])+(d[3]>=d[2])+(d[3]>d[4])+(d[3]>d[5]);
  int o4 = (d[4]>=d[0])+(d[4]>=d[1])+(d[4]>=d[2])+(d[4]>=d[3])+(d[4]>d[5]);
  int o5 = 15-(o0+o1+o2+o3+o4);
  d[o0]=e[0]; d[o1]=e[1]; d[o2]=e[2]; d[o3]=e[3]; d[o4]=e[4]; d[o5]=e[5];
}

@Rex: con gcc -O1 è inferiore a 1000 cicli, abbastanza veloce ma più lento della rete di smistamento. Qualche idea per migliorare il codice? Forse se potessimo evitare la copia dell'array ...
Kriss

@kriss: è più veloce della rete di smistamento per me con -O2. C'è qualche motivo per cui -O2 non va bene, o è più lento anche per te su -O2? Forse è una differenza nell'architettura della macchina?
Rex Kerr,

1
@Rex: scusa, a prima vista ho perso lo schema> vs> =. Funziona in ogni caso.
Kriss,

3
@kriss: Aha. Ciò non è del tutto sorprendente: ci sono molte variabili che fluttuano intorno e devono essere ordinate con cura e memorizzate nella cache nei registri e così via.
Rex Kerr,

2
@SSpoke 0+1+2+3+4+5=15Poiché uno di questi è scomparso, 15 meno la somma degli altri produce uno mancante
Glenn Teitelbaum,

35

Sembra che sia arrivato alla festa con un anno di ritardo, ma eccoci qui ...

Osservando l'assemblaggio generato da gcc 4.5.2 ho osservato che carichi e depositi sono stati effettuati per ogni scambio, il che non è realmente necessario. Sarebbe meglio caricare i 6 valori nei registri, ordinarli e salvarli in memoria. Ho ordinato che i carichi nei negozi fossero il più vicino possibile ai registri necessari e utilizzati per ultimi. Ho anche usato la macro SWAP di Steinar H. Gunderson. Aggiornamento: sono passato alla macro SWAP di Paolo Bonzini che gcc converte in qualcosa di simile a quello di Gunderson, ma gcc è in grado di ordinare meglio le istruzioni poiché non sono fornite come assembly esplicito.

Ho usato lo stesso ordine di scambio della rete di scambio riordinata fornita come la migliore, anche se potrebbe esserci un ordine migliore. Se avrò ancora un po 'di tempo, genererò e testerò un mucchio di permutazioni.

Ho modificato il codice di test per considerare oltre 4000 array e mostrare il numero medio di cicli necessari per ordinare ciascuno. Su un i5-650 ricevo ~ 34.1 cicli / ordinamento (usando -O3), rispetto alla rete di ordinamento riordinata originale ottenendo ~ 65.3 cicli / ordinamento (usando -O1, batte -O2 e -O3).

#include <stdio.h>

static inline void sort6_fast(int * d) {
#define SWAP(x,y) { int dx = x, dy = y, tmp; tmp = x = dx < dy ? dx : dy; y ^= dx ^ tmp; }
    register int x0,x1,x2,x3,x4,x5;
    x1 = d[1];
    x2 = d[2];
    SWAP(x1, x2);
    x4 = d[4];
    x5 = d[5];
    SWAP(x4, x5);
    x0 = d[0];
    SWAP(x0, x2);
    x3 = d[3];
    SWAP(x3, x5);
    SWAP(x0, x1);
    SWAP(x3, x4);
    SWAP(x1, x4);
    SWAP(x0, x3);
    d[0] = x0;
    SWAP(x2, x5);
    d[5] = x5;
    SWAP(x1, x3);
    d[1] = x1;
    SWAP(x2, x4);
    d[4] = x4;
    SWAP(x2, x3);
    d[2] = x2;
    d[3] = x3;

#undef SWAP
#undef min
#undef max
}

static __inline__ unsigned long long rdtsc(void)
{
    unsigned long long int x;
    __asm__ volatile ("rdtsc; shlq $32, %%rdx; orq %%rdx, %0" : "=a" (x) : : "rdx");
    return x;
}

void ran_fill(int n, int *a) {
    static int seed = 76521;
    while (n--) *a++ = (seed = seed *1812433253 + 12345);
}

#define NTESTS 4096
int main() {
    int i;
    int d[6*NTESTS];
    ran_fill(6*NTESTS, d);

    unsigned long long cycles = rdtsc();
    for (i = 0; i < 6*NTESTS ; i+=6) {
        sort6_fast(d+i);
    }
    cycles = rdtsc() - cycles;
    printf("Time is %.2lf\n", (double)cycles/(double)NTESTS);

    for (i = 0; i < 6*NTESTS ; i+=6) {
        if (d[i+0] > d[i+1] || d[i+1] > d[i+2] || d[i+2] > d[i+3] || d[i+3] > d[i+4] || d[i+4] > d[i+5])
            printf("d%d : %d %d %d %d %d %d\n", i,
                    d[i+0], d[i+1], d[i+2],
                    d[i+3], d[i+4], d[i+5]);
    }
    return 0;
}

Ho modificato la suite di test per riportare anche gli orologi per ordinamento ed eseguire più test (la funzione cmp è stata aggiornata per gestire anche l'overflow di numeri interi), ecco i risultati su alcune architetture diverse. Ho provato a testare una CPU AMD ma rdtsc non è affidabile sull'X6 1100T che ho a disposizione.

Clarkdale (i5-650)
==================
Direct call to qsort library function      635.14   575.65   581.61   577.76   521.12
Naive implementation (insertion sort)      538.30   135.36   134.89   240.62   101.23
Insertion Sort (Daniel Stutzbach)          424.48   159.85   160.76   152.01   151.92
Insertion Sort Unrolled                    339.16   125.16   125.81   129.93   123.16
Rank Order                                 184.34   106.58   54.74    93.24    94.09
Rank Order with registers                  127.45   104.65   53.79    98.05    97.95
Sorting Networks (Daniel Stutzbach)        269.77   130.56   128.15   126.70   127.30
Sorting Networks (Paul R)                  551.64   103.20   64.57    73.68    73.51
Sorting Networks 12 with Fast Swap         321.74   61.61    63.90    67.92    67.76
Sorting Networks 12 reordered Swap         318.75   60.69    65.90    70.25    70.06
Reordered Sorting Network w/ fast swap     145.91   34.17    32.66    32.22    32.18

Kentsfield (Core 2 Quad)
========================
Direct call to qsort library function      870.01   736.39   723.39   725.48   721.85
Naive implementation (insertion sort)      503.67   174.09   182.13   284.41   191.10
Insertion Sort (Daniel Stutzbach)          345.32   152.84   157.67   151.23   150.96
Insertion Sort Unrolled                    316.20   133.03   129.86   118.96   105.06
Rank Order                                 164.37   138.32   46.29    99.87    99.81
Rank Order with registers                  115.44   116.02   44.04    116.04   116.03
Sorting Networks (Daniel Stutzbach)        230.35   114.31   119.15   110.51   111.45
Sorting Networks (Paul R)                  498.94   77.24    63.98    62.17    65.67
Sorting Networks 12 with Fast Swap         315.98   59.41    58.36    60.29    55.15
Sorting Networks 12 reordered Swap         307.67   55.78    51.48    51.67    50.74
Reordered Sorting Network w/ fast swap     149.68   31.46    30.91    31.54    31.58

Sandy Bridge (i7-2600k)
=======================
Direct call to qsort library function      559.97   451.88   464.84   491.35   458.11
Naive implementation (insertion sort)      341.15   160.26   160.45   154.40   106.54
Insertion Sort (Daniel Stutzbach)          284.17   136.74   132.69   123.85   121.77
Insertion Sort Unrolled                    239.40   110.49   114.81   110.79   117.30
Rank Order                                 114.24   76.42    45.31    36.96    36.73
Rank Order with registers                  105.09   32.31    48.54    32.51    33.29
Sorting Networks (Daniel Stutzbach)        210.56   115.68   116.69   107.05   124.08
Sorting Networks (Paul R)                  364.03   66.02    61.64    45.70    44.19
Sorting Networks 12 with Fast Swap         246.97   41.36    59.03    41.66    38.98
Sorting Networks 12 reordered Swap         235.39   38.84    47.36    38.61    37.29
Reordered Sorting Network w/ fast swap     115.58   27.23    27.75    27.25    26.54

Nehalem (Xeon E5640)
====================
Direct call to qsort library function      911.62   890.88   681.80   876.03   872.89
Naive implementation (insertion sort)      457.69   236.87   127.68   388.74   175.28
Insertion Sort (Daniel Stutzbach)          317.89   279.74   147.78   247.97   245.09
Insertion Sort Unrolled                    259.63   220.60   116.55   221.66   212.93
Rank Order                                 140.62   197.04   52.10    163.66   153.63
Rank Order with registers                  84.83    96.78    50.93    109.96   54.73
Sorting Networks (Daniel Stutzbach)        214.59   220.94   118.68   120.60   116.09
Sorting Networks (Paul R)                  459.17   163.76   56.40    61.83    58.69
Sorting Networks 12 with Fast Swap         284.58   95.01    50.66    53.19    55.47
Sorting Networks 12 reordered Swap         281.20   96.72    44.15    56.38    54.57
Reordered Sorting Network w/ fast swap     128.34   50.87    26.87    27.91    28.02

La tua idea di variabili di registro dovrebbe essere applicata alla soluzione "Rank Order" di Rex Kerr. Dovrebbe essere più veloce e forse quindi l' -O3ottimizzazione non sarà controproducente.
cdunn2001,

1
@ cdunn2001 L'ho appena provato, non vedo miglioramenti (tranne alcuni cicli a -O0 e -Os). Guardando l'asm, sembra che gcc sia già riuscito a capire di usare i registri ed eliminare la chiamata a memcpy.
Kevin Stock

Ti dispiacerebbe aggiungere la semplice versione di swap alla tua suite di test, immagino che potrebbe essere interessante confrontarla con lo swap di assemblaggio ottimizzato a mano.
Kriss,

1
Il tuo codice utilizza ancora lo scambio di Gunderson, il mio sarebbe #define SWAP(x,y) { int oldx = x; x = x < y ? x : y; y ^= oldx ^ x; }.
Paolo Bonzini,

@Paolo Bonzini: Sì, ho intenzione di aggiungere un test case con il tuo, ma non ho ancora avuto tempo. Ma eviterò l'assemblaggio in linea.
Kriss,

15

Mi sono imbattuto in questa domanda da Google alcuni giorni fa perché avevo anche bisogno di ordinare rapidamente un array a lunghezza fissa di 6 numeri interi. Nel mio caso, tuttavia, i miei numeri interi sono solo 8 bit (anziché 32) e non ho un requisito rigoroso di utilizzare solo C. Ho pensato di condividere comunque i miei risultati, nel caso in cui possano essere utili a qualcuno ...

Ho implementato una variante di un ordinamento di rete in assembly che utilizza SSE per vettorializzare le operazioni di confronto e scambio, per quanto possibile. Sono necessari sei "passaggi" per ordinare completamente l'array. Ho usato un nuovo meccanismo per convertire direttamente i risultati di PCMPGTB (confronto vettoriale) in shuffle parametri per PSHUFB (scambio vettoriale), usando solo un PADDB (aggiunta vettoriale) e in alcuni casi anche un'istruzione PAND (bitwise AND).

Questo approccio ha avuto anche l'effetto collaterale di ottenere una funzione veramente senza rami. Non ci sono istruzioni di salto di sorta.

Sembra che questa implementazione sia circa il 38% più veloce dell'implementazione che è attualmente contrassegnata come l'opzione più veloce nella domanda ("Ordinamento delle reti 12 con Simple Swap"). Ho modificato tale implementazione per utilizzare gli charelementi dell'array durante i miei test, per rendere il confronto equo.

Devo notare che questo approccio può essere applicato a qualsiasi dimensione di array fino a 16 elementi. Mi aspetto che il vantaggio della velocità relativa rispetto alle alternative aumenti per gli array più grandi.

Il codice è scritto in MASM per processori x86_64 con SSSE3. La funzione utilizza la "nuova" convenzione di chiamata x64 di Windows. Ecco qui...

PUBLIC simd_sort_6

.DATA

ALIGN 16

pass1_shuffle   OWORD   0F0E0D0C0B0A09080706040503010200h
pass1_add       OWORD   0F0E0D0C0B0A09080706050503020200h
pass2_shuffle   OWORD   0F0E0D0C0B0A09080706030405000102h
pass2_and       OWORD   00000000000000000000FE00FEFE00FEh
pass2_add       OWORD   0F0E0D0C0B0A09080706050405020102h
pass3_shuffle   OWORD   0F0E0D0C0B0A09080706020304050001h
pass3_and       OWORD   00000000000000000000FDFFFFFDFFFFh
pass3_add       OWORD   0F0E0D0C0B0A09080706050404050101h
pass4_shuffle   OWORD   0F0E0D0C0B0A09080706050100020403h
pass4_and       OWORD   0000000000000000000000FDFD00FDFDh
pass4_add       OWORD   0F0E0D0C0B0A09080706050403020403h
pass5_shuffle   OWORD   0F0E0D0C0B0A09080706050201040300h
pass5_and       OWORD 0000000000000000000000FEFEFEFE00h
pass5_add       OWORD   0F0E0D0C0B0A09080706050403040300h
pass6_shuffle   OWORD   0F0E0D0C0B0A09080706050402030100h
pass6_add       OWORD   0F0E0D0C0B0A09080706050403030100h

.CODE

simd_sort_6 PROC FRAME

    .endprolog

    ; pxor xmm4, xmm4
    ; pinsrd xmm4, dword ptr [rcx], 0
    ; pinsrb xmm4, byte ptr [rcx + 4], 4
    ; pinsrb xmm4, byte ptr [rcx + 5], 5
    ; The benchmarked 38% faster mentioned in the text was with the above slower sequence that tied up the shuffle port longer.  Same on extract
    ; avoiding pins/extrb also means we don't need SSE 4.1, but SSSE3 CPUs without SSE4.1 (e.g. Conroe/Merom) have slow pshufb.
    movd    xmm4, dword ptr [rcx]
    pinsrw  xmm4,  word ptr [rcx + 4], 2  ; word 2 = bytes 4 and 5


    movdqa xmm5, xmm4
    pshufb xmm5, oword ptr [pass1_shuffle]
    pcmpgtb xmm5, xmm4
    paddb xmm5, oword ptr [pass1_add]
    pshufb xmm4, xmm5

    movdqa xmm5, xmm4
    pshufb xmm5, oword ptr [pass2_shuffle]
    pcmpgtb xmm5, xmm4
    pand xmm5, oword ptr [pass2_and]
    paddb xmm5, oword ptr [pass2_add]
    pshufb xmm4, xmm5

    movdqa xmm5, xmm4
    pshufb xmm5, oword ptr [pass3_shuffle]
    pcmpgtb xmm5, xmm4
    pand xmm5, oword ptr [pass3_and]
    paddb xmm5, oword ptr [pass3_add]
    pshufb xmm4, xmm5

    movdqa xmm5, xmm4
    pshufb xmm5, oword ptr [pass4_shuffle]
    pcmpgtb xmm5, xmm4
    pand xmm5, oword ptr [pass4_and]
    paddb xmm5, oword ptr [pass4_add]
    pshufb xmm4, xmm5

    movdqa xmm5, xmm4
    pshufb xmm5, oword ptr [pass5_shuffle]
    pcmpgtb xmm5, xmm4
    pand xmm5, oword ptr [pass5_and]
    paddb xmm5, oword ptr [pass5_add]
    pshufb xmm4, xmm5

    movdqa xmm5, xmm4
    pshufb xmm5, oword ptr [pass6_shuffle]
    pcmpgtb xmm5, xmm4
    paddb xmm5, oword ptr [pass6_add]
    pshufb xmm4, xmm5

    ;pextrd dword ptr [rcx], xmm4, 0    ; benchmarked with this
    ;pextrb byte ptr [rcx + 4], xmm4, 4 ; slower version
    ;pextrb byte ptr [rcx + 5], xmm4, 5
    movd   dword ptr [rcx], xmm4
    pextrw  word ptr [rcx + 4], xmm4, 2  ; x86 is little-endian, so this is the right order

    ret

simd_sort_6 ENDP

END

Puoi compilarlo in un oggetto eseguibile e collegarlo al tuo progetto C. Per istruzioni su come eseguire questa operazione in Visual Studio, puoi leggere questo articolo . Puoi usare il seguente prototipo C per chiamare la funzione dal tuo codice C:

void simd_sort_6(char *values);

Sarebbe interessante confrontare il tuo con altre proposte a livello di assemblea. Le prestazioni comparate dell'implementazione non le includono. L'uso di SSE suona comunque bene.
Kriss,

Un'altra area di ricerca futura sarebbe l'applicazione delle nuove istruzioni Intel AVX a questo problema. I vettori a 256 bit più grandi sono abbastanza grandi da contenere 8 DWORD.
Joe Crivello,

1
Invece di pxor / pinsrd xmm4, mem, 0, basta usare movd!
Peter Cordes,

14

Il codice di test è piuttosto male; trabocca l'array iniziale (le persone qui non leggono gli avvisi del compilatore?), printf sta stampando gli elementi sbagliati, usa .byte per rdtsc senza una buona ragione, c'è solo una corsa (!), non c'è nulla che controlla che il i risultati finali sono effettivamente corretti (quindi è molto facile "ottimizzare" in qualcosa di sottilmente sbagliato), i test inclusi sono molto rudimentali (niente numeri negativi?) e non c'è nulla che impedisca al compilatore di scartare l'intera funzione come codice morto.

Detto questo, è anche abbastanza facile migliorare la soluzione di rete bitonica; cambia semplicemente le cose min / max / SWAP in

#define SWAP(x,y) { int tmp; asm("mov %0, %2 ; cmp %1, %0 ; cmovg %1, %0 ; cmovg %2, %1" : "=r" (d[x]), "=r" (d[y]), "=r" (tmp) : "0" (d[x]), "1" (d[y]) : "cc"); }

e risulta circa il 65% più veloce per me (Debian gcc 4.4.5 con -O2, amd64, Core i7).


OK, il codice di prova è scadente. Sentiti libero di migliorarlo. E sì, puoi usare il codice assembly. Perché non andare fino in fondo e codificarlo completamente usando l'assemblatore x86? Potrebbe essere un po 'meno portatile, ma perché preoccuparsi?
Kriss,

Grazie per aver notato l'overflow dell'array, l'ho corretto. Altre persone potrebbero non averlo notato perché ha fatto clic sul collegamento per copiare / incollare il codice, dove non c'è overflow.
Kriss,

4
In realtà non hai nemmeno bisogno dell'assemblatore; se lasci cadere tutti i trucchi intelligenti, GCC riconoscerà la sequenza e inserirà le mosse condizionate per te: #define min (a, b) ((a <b)? a: b) #define max (a, b) ( (a <b)? b: a) #define SWAP (x, y) {int a = min (d [x], d [y]); int b = max (d [x], d [y]); d [x] = a; d [y] = b; } Ne risulta forse un po 'più lento della variante asm inline, ma è difficile da dire data la mancanza di un benchmark adeguato.
Steinar H. Gunderson,

3
... e infine, se i tuoi numeri sono float e non devi preoccuparti di NaN ecc., GCC può convertirlo in istruzioni SSE minss / maxss, che è ancora ~ 25% più veloce. Morale: elimina i trucchi intelligenti di bitfiddling e lascia che il compilatore faccia il suo lavoro. :-)
Steinar H. Gunderson,

13

Mentre mi piace molto la macro di scambio fornita:

#define min(x, y) (y ^ ((x ^ y) & -(x < y)))
#define max(x, y) (x ^ ((x ^ y) & -(x < y)))
#define SWAP(x,y) { int tmp = min(d[x], d[y]); d[y] = max(d[x], d[y]); d[x] = tmp; }

Vedo un miglioramento (che potrebbe essere un buon compilatore):

#define SWAP(x,y) { int tmp = ((x ^ y) & -(y < x)); y ^= tmp; x ^= tmp; }

Prendiamo nota di come funzionano min e max e tiriamo esplicitamente la sottoespressione comune. Questo elimina completamente le macro min e max.


Questo li riporta indietro, nota che d [y] ottiene il massimo, che è x ^ (sottoespressione comune).
Kevin Stock

Ho notato la stessa cosa; Penso che la tua implementazione sia corretta, d[x]invece di x(uguale per y), e d[y] < d[x]per la disuguaglianza qui (sì, diversa dal codice min / max).
Tyler,

Ho provato con il tuo swap, ma l'ottimizzazione locale ha effetti negativi a livello più ampio (suppongo che introduca dipendenze). E il risultato è più lento dell'altro scambio. Ma come puoi vedere con la nuova soluzione proposta, c'erano davvero molte prestazioni per ottenere l'ottimizzazione dello swap.
Kriss,

12

Non ottimizzare mai min / max senza benchmarking e guardando l'assemblaggio generato dal compilatore reale. Se lascio che GCC ottimizzi il min con istruzioni di spostamento condizionate ottengo un aumento del 33%:

#define SWAP(x,y) { int dx = d[x], dy = d[y], tmp; tmp = d[x] = dx < dy ? dx : dy; d[y] ^= dx ^ tmp; }

(280 vs. 420 cicli nel codice test). Fare max con?: È più o meno lo stesso, quasi perso nel rumore, ma quanto sopra è un po 'più veloce. Questo SWAP è più veloce sia con GCC che con Clang.

I compilatori stanno inoltre svolgendo un lavoro eccezionale nell'allocazione dei registri e nell'analisi degli alias, spostando efficacemente d [x] nelle variabili locali in anticipo e copiando alla fine solo la memoria. In effetti, lo fanno anche meglio che se lavorassi interamente con variabili locali (come d0 = d[0], d1 = d[1], d2 = d[2], d3 = d[3], d4 = d[4], d5 = d[5]). Sto scrivendo questo perché stai assumendo una forte ottimizzazione e stai ancora cercando di superare in astuzia il compilatore su min / max. :)

A proposito, ho provato Clang e GCC. Fanno la stessa ottimizzazione, ma a causa delle differenze di pianificazione i due hanno qualche variazione nei risultati, non si può dire davvero quale sia più veloce o più lento. GCC è più veloce sulle reti di smistamento, Clang sulle specie quadratiche.

Solo per completezza, sono possibili anche lo smistamento delle bolle e gli ordinamenti di inserzione. Ecco il tipo di bolla:

SWAP(0,1); SWAP(1,2); SWAP(2,3); SWAP(3,4); SWAP(4,5);
SWAP(0,1); SWAP(1,2); SWAP(2,3); SWAP(3,4);
SWAP(0,1); SWAP(1,2); SWAP(2,3);
SWAP(0,1); SWAP(1,2);
SWAP(0,1);

ed ecco l'ordinamento per inserzione:

//#define ITER(x) { if (t < d[x]) { d[x+1] = d[x]; d[x] = t; } }
//Faster on x86, probably slower on ARM or similar:
#define ITER(x) { d[x+1] ^= t < d[x] ? d[x] ^ d[x+1] : 0; d[x] = t < d[x] ? t : d[x]; }
static inline void sort6_insertion_sort_unrolled_v2(int * d){
    int t;
    t = d[1]; ITER(0);
    t = d[2]; ITER(1); ITER(0);
    t = d[3]; ITER(2); ITER(1); ITER(0);
    t = d[4]; ITER(3); ITER(2); ITER(1); ITER(0);
    t = d[5]; ITER(4); ITER(3); ITER(2); ITER(1); ITER(0);

Questo ordinamento di inserzione è più veloce di quello di Daniel Stutzbach, ed è particolarmente utile su una GPU o un computer con predicazione perché ITER può essere eseguito con solo 3 istruzioni (rispetto a 4 per SWAP). Ad esempio, ecco la t = d[2]; ITER(1); ITER(0);linea nell'assemblaggio ARM:

    MOV    r6, r2
    CMP    r6, r1
    MOVLT  r2, r1
    MOVLT  r1, r6
    CMP    r6, r0
    MOVLT  r1, r0
    MOVLT  r0, r6

Per sei elementi, l'ordinamento per inserzione è competitivo con la rete di ordinamento (12 scambi contro 15 iterazioni equilibra 4 istruzioni / scambio contro 3 istruzioni / iterazione); la bolla ovviamente è più lenta. Ma non sarà vero quando la dimensione aumenta, poiché l'ordinamento per inserzione è O (n ^ 2) mentre le reti di ordinamento sono O (n log n).


1
Più o meno correlati: ho inviato un rapporto a GCC in modo che potesse implementare l'ottimizzazione direttamente nel compilatore. Non sei sicuro che sarà fatto, ma almeno puoi seguire come si evolve.
Morwenn,

11

Ho portato la suite di test su una macchina con architettura PPC che non riesco a identificare (non ho dovuto toccare il codice, basta aumentare le iterazioni del test, usare 8 casi di test per evitare di inquinare i risultati con le mod e sostituire l'RDTSC specifico x86):

Chiamata diretta alla funzione di libreria qsort : 101

Implementazione ingenua (ordinamento inserzione) : 299

Inserimento ordinamento (Daniel Stutzbach) : 108

Inserimento ordinamento non srotolato : 51

Reti di smistamento (Daniel Stutzbach) : 26

Reti di classificazione (Paul R) : 85

Ordinamento delle reti 12 con Fast Swap : 117

Reti ordinate 12 Scambio riordinato : 116

Ordine di rango : 56


1
Molto interessante. Sembra che lo swap senza rami sia una cattiva idea su PPC. Potrebbe anche essere un effetto correlato al compilatore. Quale è stato usato?
Kriss,

È un ramo del compilatore gcc - la logica minima e massima probabilmente non è senza rami - ispezionerò lo smontaggio e ti farò sapere, ma a meno che il compilatore non sia abbastanza intelligente includendo qualcosa come x <y senza un se diventa ancora un ramo - su x86 / x64 l'istruzione CMOV potrebbe evitarlo, ma non esiste tale istruzione per i valori a virgola fissa su PPC, solo float. Domani potrei dilettarmi con questo e farvi sapere - ricordo che c'era un min / max branchless molto più semplice nella sorgente Winamp AVS, ma iirc era solo per i float - ma potrebbe essere un buon inizio per un approccio veramente branchless.
jheriko,

4
Ecco un branchless min / max per PPC con ingressi senza segno: subfc r5,r4,r3; subfe r6,r6,r6; andc r6,r5,r6; add r4,r6,r4; subf r3,r6,r3. r3 / r4 sono ingressi, r5 / r6 sono registri scratch, sull'uscita r3 ottiene il minimo e r4 ottiene il massimo. Dovrebbe essere decentemente programmabile a mano. L'ho trovato con il superottimizzatore GNU, partendo da sequenze min e max di 4 istruzioni e cercando manualmente due che potevano essere combinate. Per gli input con segno, puoi ovviamente aggiungere 0x80000000 a tutti gli elementi all'inizio e sottrarlo nuovamente alla fine, quindi lavorare come se fossero senza segno.
Paolo Bonzini,

7

Uno scambio XOR può essere utile nelle funzioni di scambio.

void xorSwap (int *x, int *y) {
     if (*x != *y) {
         *x ^= *y;
         *y ^= *x;
         *x ^= *y;
     }
 }

L'if può causare troppe divergenze nel tuo codice, ma se hai la garanzia che tutti i tuoi ints sono unici questo potrebbe essere utile.


1
xor swap funziona anche per valori uguali ... x ^ = y imposta x su 0, y ^ = x lascia y come y (== x), x ^ = y imposta x su y
jheriko

11
Quando non funziona è quando xe ypuntare alla stessa posizione.
Hobbs,

Ad ogni modo, quando usati con le reti di smistamento, non chiamiamo mai entrambi con xe y puntando alla stessa posizione. C'è ancora da trovare un modo per evitare i test che è maggiore per ottenere lo stesso effetto dello scambio senza rami. Ho un'idea per raggiungerlo.
Kriss,

5

Non vedo l'ora di provare la mia mano e imparare da questi esempi, ma prima alcuni cronometri dal mio PPC Powerbook G4 da 1,5 GHz con 1 GB di RAM DDR RAM. (Ho preso in prestito un timer simile a rdtsc simile per PPC da http://www.mcs.anl.gov/~kazutomo/rdtsc.html per i tempi.) Ho eseguito il programma alcune volte e i risultati assoluti sono variati ma costantemente il test più veloce è stato "Insertion Sort (Daniel Stutzbach)", con "Insertion Sort Unrolled" al secondo posto.

Ecco l'ultimo set di volte:

**Direct call to qsort library function** : 164
**Naive implementation (insertion sort)** : 138
**Insertion Sort (Daniel Stutzbach)**     : 85
**Insertion Sort Unrolled**               : 97
**Sorting Networks (Daniel Stutzbach)**   : 457
**Sorting Networks (Paul R)**             : 179
**Sorting Networks 12 with Fast Swap**    : 238
**Sorting Networks 12 reordered Swap**    : 236
**Rank Order**                            : 116

4

Ecco il mio contributo a questo thread: uno shellsort a 1, 4 gap ottimizzato per un vettore int a 6 membri (valp) contenente valori univoci.

void shellsort (int *valp)
{      
  int c,a,*cp,*ip=valp,*ep=valp+5;

  c=*valp;    a=*(valp+4);if (c>a) {*valp=    a;*(valp+4)=c;}
  c=*(valp+1);a=*(valp+5);if (c>a) {*(valp+1)=a;*(valp+5)=c;}

  cp=ip;    
  do
  {
    c=*cp;
    a=*(cp+1);
    do
    {
      if (c<a) break;

      *cp=a;
      *(cp+1)=c;
      cp-=1;
      c=*cp;
    } while (cp>=valp);
    ip+=1;
    cp=ip;
  } while (ip<ep);
}

Sul mio portatile HP dv7-3010so con un Athlon M300 @ 2 Ghz dual-core (memoria DDR2) si esegue in 165 cicli di clock. Questa è una media calcolata dal tempismo di ogni sequenza unica (6! / 720 in tutto). Compilato per Win32 usando OpenWatcom 1.8. Il ciclo è essenzialmente un ordinamento di inserzione ed è lungo 16 istruzioni / 37 byte.

Non ho un ambiente a 64 bit su cui compilare.


simpatico. Lo aggiungerò al testuite più lungo
Kriss

3

Se qui l'ordinamento per inserzione è ragionevolmente competitivo, consiglierei di provare uno shellsort. Temo che 6 elementi siano probabilmente troppo piccoli per essere tra i migliori, ma potrebbe valere la pena provarli.

Codice di esempio, non testato, non indebolito, ecc. Si desidera ottimizzare la sequenza inc = 4 e inc - = 3 per trovare l'ottimale (provare ad esempio inc = 2, inc - = 1).

static __inline__ int sort6(int * d) {
    char j, i;
    int tmp;
    for (inc = 4; inc > 0; inc -= 3) {
        for (i = inc; i < 5; i++) {
            tmp = a[i];
            j = i;
            while (j >= inc && a[j - inc] > tmp) {
                a[j] = a[j - inc];
                j -= inc;
            }
            a[j] = tmp;
        }
    }
}

Non credo che questo vincerà, ma se qualcuno pubblica una domanda sull'ordinamento di 10 elementi, chissà ...

Secondo Wikipedia questo può anche essere combinato con le reti di smistamento: Pratt, V (1979). Shellsort e reti di smistamento (tesi di laurea in scienze informatiche eccezionali). Ghirlanda. ISBN 0-824-04406-1


sentiti libero di proporre un po 'di implementazione :-)
Kriss,

Proposta aggiunta. Goditi i bug.
gcp,

3

So di essere in ritardo, ma ero interessato a sperimentare alcune soluzioni diverse. Innanzitutto, ho ripulito quella pasta, l'ho fatta compilare e l'ho messa in un repository. Ho tenuto alcune soluzioni indesiderabili come vicoli ciechi in modo che altri non ci provassero. Tra queste c'era la mia prima soluzione, che ha cercato di garantire che x1> x2 fosse calcolato una volta. Dopo l'ottimizzazione, non è più veloce delle altre versioni semplici.

Ho aggiunto una versione ciclica dell'ordinamento dei ranghi, poiché la mia applicazione di questo studio è per l'ordinamento di 2-8 voci, quindi poiché ci sono un numero variabile di argomenti, è necessario un ciclo. Questo è anche il motivo per cui ho ignorato le soluzioni di rete di ordinamento.

Il codice di test non ha verificato che i duplicati fossero gestiti correttamente, quindi mentre le soluzioni esistenti erano tutte corrette, ho aggiunto un caso speciale al codice di test per assicurarmi che i duplicati fossero gestiti correttamente.

Quindi, ho scritto un tipo di inserimento che è interamente nei registri AVX. Sulla mia macchina è del 25% più veloce rispetto agli altri tipi di inserzione, ma più lento del 100% rispetto all'ordine di classificazione. L'ho fatto solo per esperimento e non mi aspettavo che questo fosse migliore a causa della ramificazione in ordine di inserzione.

static inline void sort6_insertion_sort_avx(int* d) {
    __m256i src = _mm256_setr_epi32(d[0], d[1], d[2], d[3], d[4], d[5], 0, 0);
    __m256i index = _mm256_setr_epi32(0, 1, 2, 3, 4, 5, 6, 7);
    __m256i shlpermute = _mm256_setr_epi32(7, 0, 1, 2, 3, 4, 5, 6);
    __m256i sorted = _mm256_setr_epi32(d[0], INT_MAX, INT_MAX, INT_MAX,
            INT_MAX, INT_MAX, INT_MAX, INT_MAX);
    __m256i val, gt, permute;
    unsigned j;
     // 8 / 32 = 2^-2
#define ITER(I) \
        val = _mm256_permutevar8x32_epi32(src, _mm256_set1_epi32(I));\
        gt =  _mm256_cmpgt_epi32(sorted, val);\
        permute =  _mm256_blendv_epi8(index, shlpermute, gt);\
        j = ffs( _mm256_movemask_epi8(gt)) >> 2;\
        sorted = _mm256_blendv_epi8(_mm256_permutevar8x32_epi32(sorted, permute),\
                val, _mm256_cmpeq_epi32(index, _mm256_set1_epi32(j)))
    ITER(1);
    ITER(2);
    ITER(3);
    ITER(4);
    ITER(5);
    int x[8];
    _mm256_storeu_si256((__m256i*)x, sorted);
    d[0] = x[0]; d[1] = x[1]; d[2] = x[2]; d[3] = x[3]; d[4] = x[4]; d[5] = x[5];
#undef ITER
}

Quindi, ho scritto un ordinamento per gradi usando AVX. Ciò corrisponde alla velocità delle altre soluzioni di ordinamento, ma non è più veloce. Il problema qui è che posso solo calcolare gli indici con AVX, e quindi devo fare una tabella di indici. Questo perché il calcolo si basa sulla destinazione anziché sulla sorgente. Vedere Conversione da indici basati sulla sorgente in indici basati sulla destinazione

static inline void sort6_rank_order_avx(int* d) {
    __m256i ror = _mm256_setr_epi32(5, 0, 1, 2, 3, 4, 6, 7);
    __m256i one = _mm256_set1_epi32(1);
    __m256i src = _mm256_setr_epi32(d[0], d[1], d[2], d[3], d[4], d[5], INT_MAX, INT_MAX);
    __m256i rot = src;
    __m256i index = _mm256_setzero_si256();
    __m256i gt, permute;
    __m256i shl = _mm256_setr_epi32(1, 2, 3, 4, 5, 6, 6, 6);
    __m256i dstIx = _mm256_setr_epi32(0,1,2,3,4,5,6,7);
    __m256i srcIx = dstIx;
    __m256i eq = one;
    __m256i rotIx = _mm256_setzero_si256();
#define INC(I)\
    rot = _mm256_permutevar8x32_epi32(rot, ror);\
    gt = _mm256_cmpgt_epi32(src, rot);\
    index = _mm256_add_epi32(index, _mm256_and_si256(gt, one));\
    index = _mm256_add_epi32(index, _mm256_and_si256(eq,\
                _mm256_cmpeq_epi32(src, rot)));\
    eq = _mm256_insert_epi32(eq, 0, I)
    INC(0);
    INC(1);
    INC(2);
    INC(3);
    INC(4);
    int e[6];
    e[0] = d[0]; e[1] = d[1]; e[2] = d[2]; e[3] = d[3]; e[4] = d[4]; e[5] = d[5];
    int i[8];
    _mm256_storeu_si256((__m256i*)i, index);
    d[i[0]] = e[0]; d[i[1]] = e[1]; d[i[2]] = e[2]; d[i[3]] = e[3]; d[i[4]] = e[4]; d[i[5]] = e[5];
}

Il repository può essere trovato qui: https://github.com/eyepatchParrot/sort6/


1
È possibile utilizzare vmovmskpssu vettori interi (con un cast per mantenere felici gli intrinseci), evitando la necessità di spostare a destra il risultato bitscan ( ffs).
Peter Cordes,

1
È possibile aggiungere condizionalmente 1 in base a un cmpgtrisultato sottraendolo , invece di mascherarlo con set1(1). ad esempio index = _mm256_sub_epi32(index, gt)faindex -= -1 or 0;
Peter Cordes

1
eq = _mm256_insert_epi32(eq, 0, I)non è un modo efficiente di azzerare un elemento se lo si compila come scritto (specialmente per gli elementi al di fuori del 4 basso, perché vpinsrdè disponibile solo con una destinazione XMM; gli indici superiori a 3 devono essere emulati). Invece, _mm256_blend_epi32( vpblendd) con un vettore azzerato. vpblenddè un'istruzione single-uop che funziona su qualsiasi porta, rispetto a un shuffle che necessita della porta 5 su CPU Intel. ( agner.org/optimize ).
Peter Cordes,

1
Inoltre, potresti considerare di generare i rotvettori con shuffle diversi dalla stessa sorgente, o almeno eseguire 2 catene di dep in parallelo che usi alternativamente, invece di una singola catena di dep attraverso un shuffle di attraversamento di corsia (latenza di 3 cicli). Ciò aumenterà ILP all'interno di un singolo ordinamento. Le catene 2 dep limitano il numero di costanti vettoriali a un numero ragionevole, solo 2: 1 per una rotazione e una per 2 fasi di rotazione combinate.
Peter Cordes,

2

Questa domanda sta diventando piuttosto vecchia, ma in realtà ho dovuto risolvere lo stesso problema in questi giorni: agoritmi veloci per ordinare piccoli array. Ho pensato che sarebbe stata una buona idea condividere le mie conoscenze. Mentre ho iniziato a utilizzare le reti di ordinamento, sono finalmente riuscito a trovare altri algoritmi per i quali il numero totale di confronti eseguiti per ordinare ogni permutazione di 6 valori era inferiore rispetto alle reti di ordinamento e inferiore rispetto all'ordinamento per inserzione. Non ho contato il numero di swap; Mi aspetterei che sia approssimativamente equivalente (forse un po 'più alto a volte).

L'algoritmo sort6utilizza l'algoritmo sort4che utilizza l'algoritmo sort3. Ecco l'implementazione in un formato C ++ leggero (l'originale è pesante per i modelli in modo che possa funzionare con qualsiasi iteratore ad accesso casuale e qualsiasi funzione di confronto adatta).

Ordinamento di 3 valori

Il seguente algoritmo è un ordinamento di inserimento non srotolato. Quando devono essere eseguiti due swap (6 incarichi), utilizza invece 4 incarichi:

void sort3(int* array)
{
    if (array[1] < array[0]) {
        if (array[2] < array[0]) {
            if (array[2] < array[1]) {
                std::swap(array[0], array[2]);
            } else {
                int tmp = array[0];
                array[0] = array[1];
                array[1] = array[2];
                array[2] = tmp;
            }
        } else {
            std::swap(array[0], array[1]);
        }
    } else {
        if (array[2] < array[1]) {
            if (array[2] < array[0]) {
                int tmp = array[2];
                array[2] = array[1];
                array[1] = array[0];
                array[0] = tmp;
            } else {
                std::swap(array[1], array[2]);
            }
        }
    }
}

Sembra un po 'complesso perché l'ordinamento ha più o meno un ramo per ogni possibile permutazione dell'array, usando 2 ~ 3 confronti e al massimo 4 assegnazioni per ordinare i tre valori.

Ordinamento di 4 valori

Questo chiama sort3quindi esegue un ordinamento di inserimento non srotolato con l'ultimo elemento dell'array:

void sort4(int* array)
{
    // Sort the first 3 elements
    sort3(array);

    // Insert the 4th element with insertion sort 
    if (array[3] < array[2]) {
        std::swap(array[2], array[3]);
        if (array[2] < array[1]) {
            std::swap(array[1], array[2]);
            if (array[1] < array[0]) {
                std::swap(array[0], array[1]);
            }
        }
    }
}

Questo algoritmo esegue da 3 a 6 confronti e al massimo 5 scambi. È facile srotolare un ordinamento di inserzione, ma useremo un altro algoritmo per l'ultimo ordinamento ...

Ordinamento di 6 valori

Questo utilizza una versione srotolata di quello che ho chiamato un ordinamento a doppio inserimento . Il nome non è eccezionale, ma è abbastanza descrittivo, ecco come funziona:

  • Ordina tutto tranne il primo e l'ultimo elemento dell'array.
  • Scambia il primo e gli elementi dell'array se il primo è maggiore dell'ultimo.
  • Inserisci il primo elemento nella sequenza ordinata dalla parte anteriore, quindi l'ultimo elemento dalla parte posteriore.

Dopo lo scambio, il primo elemento è sempre più piccolo dell'ultimo, il che significa che, quando li si inserisce nella sequenza ordinata, non ci saranno più di N confronti per inserire i due elementi nel caso peggiore: ad esempio, se il il primo elemento è stato inserito nella terza posizione, quindi l'ultimo non può essere inserito più in basso della quarta posizione.

void sort6(int* array)
{
    // Sort everything but first and last elements
    sort4(array+1);

    // Switch first and last elements if needed
    if (array[5] < array[0]) {
        std::swap(array[0], array[5]);
    }

    // Insert first element from the front
    if (array[1] < array[0]) {
        std::swap(array[0], array[1]);
        if (array[2] < array[1]) {
            std::swap(array[1], array[2]);
            if (array[3] < array[2]) {
                std::swap(array[2], array[3]);
                if (array[4] < array[3]) {
                    std::swap(array[3], array[4]);
                }
            }
        }
    }

    // Insert last element from the back
    if (array[5] < array[4]) {
        std::swap(array[4], array[5]);
        if (array[4] < array[3]) {
            std::swap(array[3], array[4]);
            if (array[3] < array[2]) {
                std::swap(array[2], array[3]);
                if (array[2] < array[1]) {
                    std::swap(array[1], array[2]);
                }
            }
        }
    }
}

I miei test su ogni permutazione di 6 valori mostrano sempre che questo algoritmo esegue sempre tra 6 e 13 confronti. Non ho calcolato il numero di swap eseguiti, ma non mi aspetto che sia superiore a 11 nel peggiore dei casi.

Spero che questo aiuti, anche se questa domanda potrebbe non rappresentare più un vero problema :)

EDIT: dopo averlo inserito nel benchmark fornito, è chiaramente più lento della maggior parte delle alternative interessanti. Tende a funzionare un po 'meglio del tipo di inserzione non srotolato, ma è praticamente tutto. Fondamentalmente, non è il miglior ordinamento per numeri interi ma potrebbe essere interessante per i tipi con un'operazione di confronto costosa.


Questi sono carini Poiché il problema risolto è vecchio di molti decenni, probabilmente vecchio quanto una programmazione in C, la questione ora ha quasi 5 anni non sembra molto rilevante.
Kriss,

Dovresti dare un'occhiata al modo in cui le altre risposte sono cronometrate. Il punto è che con un set di dati così piccolo che conta confronti o addirittura confronti e swap non dice davvero quanto sia veloce un algoritmo (sostanzialmente l'ordinamento di 6 in è sempre O (1) perché O (6 * 6) è O (1)). L'attuale più veloce delle soluzioni precedentemente proposte sta trovando immediatamente la posizione di ciascun valore usando un grande confronto (di RexKerr).
Kriss,

@kriss Ora è il più veloce? Dalla mia lettura dei risultati, l'approccio alle reti di smistamento è stato il più veloce, il mio cattivo. È anche vero che la mia soluzione proviene dalla mia libreria generica e che non sto sempre confrontando numeri interi, né sempre usando operator<per il confronto. Oltre al conteggio oggettivo di confronti e scambi, ho anche opportunamente sincronizzato i miei algoritmi; questa soluzione era la più veloce generica, ma in effetti mi mancava quella di @ RexKerr. Lo proverò :)
Morwenn,

La soluzione di RexKerr (Order Rank) è diventata la più veloce sull'architettura X86 dal compilatore gcc 4.2.3 (e a partire da gcc 4.9 è diventata quasi due volte più veloce della seconda migliore). Ma dipende fortemente dalle ottimizzazioni del compilatore e potrebbe non essere vero su altre architetture.
Kriss,

@kriss È interessante da sapere. E potrei davvero più differenze con -O3. Immagino che adotterò quindi un'altra strategia per la mia libreria di ordinamento: fornire tre tipi di algoritmi per avere un basso numero di confronti, un basso numero di swap o potenzialmente le migliori prestazioni. Almeno, ciò che accade sarà trasparente per il lettore. Grazie per le tue intuizioni :)
Morwenn,

1

Credo che ci siano due parti nella tua domanda.

  • Il primo è determinare l'algoritmo ottimale. Questo viene fatto - almeno in questo caso - eseguendo il ciclo attraverso ogni possibile ordine (non ce ne sono molti) che consente di calcolare la deviazione minima, massima, media e standard esatta di confronti e scambi. Avere anche un secondo classificato o due utili.
  • Il secondo è ottimizzare l'algoritmo. Si può fare molto per convertire esempi di codici di libri di testo in algoritmi di vita reale snelli e meschini. Se ti rendi conto che un algoritmo non può essere ottimizzato nella misura richiesta, prova un secondo classificato.

Non mi preoccuperei troppo di svuotare le condutture (supponendo l'attuale x86): la previsione del ramo ha fatto molta strada. Ciò di cui mi preoccuperei è assicurarmi che il codice e i dati rientrino in una riga della cache ciascuno (forse due per il codice). Una volta lì, le latenze di recupero sono piacevolmente basse, il che compenserà qualsiasi stallo. Significa anche che il tuo ciclo interno avrà forse una decina di istruzioni, il che è esattamente dove dovrebbe essere (ci sono due diversi cicli interni nel mio algoritmo di ordinamento, rispettivamente 10 istruzioni / 22 byte e 9/22 di lunghezza). Supponendo che il codice non contenga div, puoi essere sicuro che sarà velocissimo.


Non sono sicuro di come capire la tua risposta. Innanzitutto non capisco affatto quale algoritmo stai proponendo? E come potrebbe essere ottimale se dovessi passare in rassegna 720 possibili ordini (le risposte esistenti richiedono molto meno di 720 cicli). Se si dispone di input casuali, non riesco a immaginare (anche a livello teorico) come la previsione del ramo potrebbe funzionare meglio di 50-50, tranne se non importa a tutti i dati di input. Inoltre, è probabile che la maggior parte delle buone soluzioni già proposte funzionino con dati e codice nella cache. Ma forse ho completamente frainteso la tua risposta. Ti dispiace mostrare un po 'di codice?
kriss

Quello che intendevo dire era che ci sono solo 720 (6!) Combinazioni diverse di 6 numeri interi e eseguendole tutte attraverso gli algoritmi candidati puoi determinare molte cose come ho detto - questa è la parte teorica. La parte pratica è la messa a punto di quell'algoritmo da eseguire nel minor numero di cicli di clock possibile. Il mio punto di partenza per l'ordinamento di 6 numeri interi è uno shellsort gap 1, 4. Il gap 4 apre la strada per una buona previsione del ramo nel gap 1.
Olof Forshell

Lo shellsort 1, 4 gap per 6! combinazioni uniche (che iniziano con 012345 e terminano con 543210) avranno il caso migliore di 7 confronti e 0 scambi e il peggiore di 14 confronti e 10 scambi. Il caso medio è di circa 11,14 confronti e 6 scambi.
Olof Forshell

1
Non ottengo la "distribuzione casuale regolare" - quello che sto facendo è testare ogni possibile combinazione e determinare statistiche min / medie / massime. Shellsort è una serie di tipi di inserzione di incrementi decrescenti in modo tale che l'incremento finale - 1 - fa molto meno lavoro rispetto a se eseguito da solo come in un ordinamento di inserzione puro. Per quanto riguarda il conteggio dell'orologio, il mio algoritmo richiede in media 406 cicli di clock e questo include la raccolta di statistiche e l'esecuzione di due chiamate alla routine di ordinamento effettiva, una per ogni intervallo. Questo è su un cellulare Athlon M300, compilatore OpenWatcom.
Olof Forshell

1
"distribuzione casuale regolare" significa che ogni combinazione di dati effettivi ordinati potrebbe non essere della stessa probabilità. Se ogni combinazione non ha la stessa probabilità, le tue statistiche sono infrante perché la media deve tenere conto di quante volte è probabile che si verifichi una determinata distribuzione. Per il conteggio dell'orologio, se provi qualsiasi altra implementazione di questo tipo (link forniti sopra) ed eseguilo sul tuo sistema di test avremo una base per il confronto e vedrai quanto bene il tuo prescelto esegue.
Kriss

1

So che questa è una vecchia domanda.

Ma ho appena scritto un diverso tipo di soluzione che voglio condividere.
Utilizzando nient'altro che MIN MAX nidificato,

Non è veloce in quanto utilizza 114 di ciascuno,
potrebbe ridurlo a 75 piuttosto semplicemente -> pastebin

Ma poi non è più puramente min max.

Ciò che potrebbe funzionare è fare min / max su più numeri contemporaneamente con AVX

Riferimento PMINSW

#include <stdio.h>

static __inline__ int MIN(int a, int b){
int result =a;
__asm__ ("pminsw %1, %0" : "+x" (result) : "x" (b));
return result;
}
static __inline__ int MAX(int a, int b){
int result = a;
__asm__ ("pmaxsw %1, %0" : "+x" (result) : "x" (b));
return result;
}
static __inline__ unsigned long long rdtsc(void){
  unsigned long long int x;
__asm__ volatile (".byte 0x0f, 0x31" :
  "=A" (x));
  return x;
}

#define MIN3(a, b, c) (MIN(MIN(a,b),c))
#define MIN4(a, b, c, d) (MIN(MIN(a,b),MIN(c,d)))

static __inline__ void sort6(int * in) {
  const int A=in[0], B=in[1], C=in[2], D=in[3], E=in[4], F=in[5];

  in[0] = MIN( MIN4(A,B,C,D),MIN(E,F) );

  const int
  AB = MAX(A, B),
  AC = MAX(A, C),
  AD = MAX(A, D),
  AE = MAX(A, E),
  AF = MAX(A, F),
  BC = MAX(B, C),
  BD = MAX(B, D),
  BE = MAX(B, E),
  BF = MAX(B, F),
  CD = MAX(C, D),
  CE = MAX(C, E),
  CF = MAX(C, F),
  DE = MAX(D, E),
  DF = MAX(D, F),
  EF = MAX(E, F);

  in[1] = MIN4 (
  MIN4( AB, AC, AD, AE ),
  MIN4( AF, BC, BD, BE ),
  MIN4( BF, CD, CE, CF ),
  MIN3( DE, DF, EF)
  );

  const int
  ABC = MAX(AB,C),
  ABD = MAX(AB,D),
  ABE = MAX(AB,E),
  ABF = MAX(AB,F),
  ACD = MAX(AC,D),
  ACE = MAX(AC,E),
  ACF = MAX(AC,F),
  ADE = MAX(AD,E),
  ADF = MAX(AD,F),
  AEF = MAX(AE,F),
  BCD = MAX(BC,D),
  BCE = MAX(BC,E),
  BCF = MAX(BC,F),
  BDE = MAX(BD,E),
  BDF = MAX(BD,F),
  BEF = MAX(BE,F),
  CDE = MAX(CD,E),
  CDF = MAX(CD,F),
  CEF = MAX(CE,F),
  DEF = MAX(DE,F);

  in[2] = MIN( MIN4 (
  MIN4( ABC, ABD, ABE, ABF ),
  MIN4( ACD, ACE, ACF, ADE ),
  MIN4( ADF, AEF, BCD, BCE ),
  MIN4( BCF, BDE, BDF, BEF )),
  MIN4( CDE, CDF, CEF, DEF )
  );


  const int
  ABCD = MAX(ABC,D),
  ABCE = MAX(ABC,E),
  ABCF = MAX(ABC,F),
  ABDE = MAX(ABD,E),
  ABDF = MAX(ABD,F),
  ABEF = MAX(ABE,F),
  ACDE = MAX(ACD,E),
  ACDF = MAX(ACD,F),
  ACEF = MAX(ACE,F),
  ADEF = MAX(ADE,F),
  BCDE = MAX(BCD,E),
  BCDF = MAX(BCD,F),
  BCEF = MAX(BCE,F),
  BDEF = MAX(BDE,F),
  CDEF = MAX(CDE,F);

  in[3] = MIN4 (
  MIN4( ABCD, ABCE, ABCF, ABDE ),
  MIN4( ABDF, ABEF, ACDE, ACDF ),
  MIN4( ACEF, ADEF, BCDE, BCDF ),
  MIN3( BCEF, BDEF, CDEF )
  );

  const int
  ABCDE= MAX(ABCD,E),
  ABCDF= MAX(ABCD,F),
  ABCEF= MAX(ABCE,F),
  ABDEF= MAX(ABDE,F),
  ACDEF= MAX(ACDE,F),
  BCDEF= MAX(BCDE,F);

  in[4]= MIN (
  MIN4( ABCDE, ABCDF, ABCEF, ABDEF ),
  MIN ( ACDEF, BCDEF )
  );

  in[5] = MAX(ABCDE,F);
}

int main(int argc, char ** argv) {
  int d[6][6] = {
    {1, 2, 3, 4, 5, 6},
    {6, 5, 4, 3, 2, 1},
    {100, 2, 300, 4, 500, 6},
    {100, 2, 3, 4, 500, 6},
    {1, 200, 3, 4, 5, 600},
    {1, 1, 2, 1, 2, 1}
  };

  unsigned long long cycles = rdtsc();
  for (int i = 0; i < 6; i++) {
    sort6(d[i]);
  }
  cycles = rdtsc() - cycles;
  printf("Time is %d\n", (unsigned)cycles);

  for (int i = 0; i < 6; i++) {
    printf("d%d : %d %d %d %d %d %d\n", i,
     d[i][0], d[i][1], d[i][2],
     d[i][3], d[i][4], d[i][5]);
  }
}

EDIT:
soluzione di ordine di rango ispirata a Rex Kerr, molto più veloce del pasticcio sopra

static void sort6(int *o) {
const int 
A=o[0],B=o[1],C=o[2],D=o[3],E=o[4],F=o[5];
const unsigned char
AB = A>B, AC = A>C, AD = A>D, AE = A>E,
          BC = B>C, BD = B>D, BE = B>E,
                    CD = C>D, CE = C>E,
                              DE = D>E,
a =          AB + AC + AD + AE + (A>F),
b = 1 - AB      + BC + BD + BE + (B>F),
c = 2 - AC - BC      + CD + CE + (C>F),
d = 3 - AD - BD - CD      + DE + (D>F),
e = 4 - AE - BE - CE - DE      + (E>F);
o[a]=A; o[b]=B; o[c]=C; o[d]=D; o[e]=E;
o[15-a-b-c-d-e]=F;
}

1
sempre bello vedere nuove soluzioni. Sembra che siano possibili alcune facili ottimizzazioni. Alla fine potrebbe non risultare così diverso dalle reti di ordinamento.
Kriss,

Sì, il numero di MIN e MAX potrebbe eventualmente essere ridotto, ad esempio MIN (AB, CD) si ripete alcune volte, ma penso che ridurli molto sarà difficile. Ho aggiunto i tuoi casi di test.
PrincePolka,

pmin / maxsw operano su interi con segno a 16 bit compressi ( int16_t). Ma la tua funzione C afferma che ordina una matrice di int(che è a 32 bit in tutte le implementazioni C che supportano quella asmsintassi). Hai provato con solo numeri interi piccoli positivi che hanno solo 0 nelle loro metà alte? Funzionerà ... Per intte è necessario SSE4.1 pmin/maxsd(d = dword). felixcloutier.com/x86/pminsd:pminsq o pminusdper uint32_t.
Peter Cordes,

1

Ho scoperto che almeno sul mio sistema, le funzioni sort6_iterator()e quelle sort6_iterator_local()definite di seguito erano entrambe altrettanto veloci, e spesso notevolmente più veloci, del detentore del record corrente sopra:

#define MIN(x, y) (x<y?x:y)
#define MAX(x, y) (x<y?y:x)

template<class IterType> 
inline void sort6_iterator(IterType it) 
{
#define SWAP(x,y) { const auto a = MIN(*(it + x), *(it + y)); \
  const auto b = MAX(*(it + x), *(it + y)); \
  *(it + x) = a; *(it + y) = b; }

  SWAP(1, 2) SWAP(4, 5)
  SWAP(0, 2) SWAP(3, 5)
  SWAP(0, 1) SWAP(3, 4)
  SWAP(1, 4) SWAP(0, 3)
  SWAP(2, 5) SWAP(1, 3)
  SWAP(2, 4)
  SWAP(2, 3)
#undef SWAP
}

Ho passato questa funzione a un std::vectoriteratore nel mio codice di temporizzazione.

Sospetto (da commenti come questo e altrove) che l'uso di iteratori fornisca a g ++ certe assicurazioni su cosa può e non può accadere alla memoria a cui fa riferimento l'iteratore, che altrimenti non avrebbe e sono queste assicurazioni che consentono a g ++ di ottimizzare meglio il codice di ordinamento (ad es. con i puntatori, il compilatore non può essere sicuro che tutti i puntatori puntino a posizioni di memoria diverse). Se ricordo bene, anche questo fa parte del motivo per cui così tanti algoritmi STL, comestd::sort() , hanno generalmente prestazioni così oscene.

Inoltre, sort6_iterator()è alcune volte (di nuovo, a seconda del contesto in cui è chiamata la funzione) costantemente superato dalla seguente funzione di ordinamento, che copia i dati in variabili locali prima di loro ordinamento. 1 Si noti che dal momento che sono definite solo 6 variabili locali, se queste variabili locali sono primitive, è probabile che non vengano mai effettivamente archiviate nella RAM e vengano invece memorizzate nei registri della CPU solo fino alla fine della chiamata di funzione, il che aiuta a fare questo ordinamento funzione veloce. (Aiuta anche il compilatore a sapere che variabili locali distinte hanno posizioni distinte in memoria).

template<class IterType> 
inline void sort6_iterator_local(IterType it) 
{
#define SWAP(x,y) { const auto a = MIN(data##x, data##y); \
  const auto b = MAX(data##x, data##y); \
  data##x = a; data##y = b; }
//DD = Define Data
#define DD1(a)   auto data##a = *(it + a);
#define DD2(a,b) auto data##a = *(it + a), data##b = *(it + b);
//CB = Copy Back
#define CB(a) *(it + a) = data##a;

  DD2(1,2)    SWAP(1, 2)
  DD2(4,5)    SWAP(4, 5)
  DD1(0)      SWAP(0, 2)
  DD1(3)      SWAP(3, 5)
  SWAP(0, 1)  SWAP(3, 4)
  SWAP(1, 4)  SWAP(0, 3)   CB(0)
  SWAP(2, 5)  CB(5)
  SWAP(1, 3)  CB(1)
  SWAP(2, 4)  CB(4)
  SWAP(2, 3)  CB(2)        CB(3)
#undef CB
#undef DD2
#undef DD1
#undef SWAP
}

Si noti che definisce SWAP()come segue alcune volte risultati leggermente migliori prestazioni anche se la maggior parte del tempo si traduce in prestazioni leggermente peggiore o una differenza trascurabile delle prestazioni.

#define SWAP(x,y) { const auto a = MIN(data##x, data##y); \
  data##y = MAX(data##x, data##y); \
  data##x = a; }

Se si desidera solo un algoritmo di ordinamento che sui tipi di dati primitivi, gcc -O3 è decisamente buoni ad ottimizzare non importa quale contesto la chiamata alla funzione di ordinamento appare nel 1 poi, a seconda di come si passa l'ingresso, provare una delle seguenti due algoritmi:

template<class T> inline void sort6(T it) {
#define SORT2(x,y) {if(data##x>data##y){auto a=std::move(data##y);data##y=std::move(data##x);data##x=std::move(a);}}
#define DD1(a)   register auto data##a=*(it+a);
#define DD2(a,b) register auto data##a=*(it+a);register auto data##b=*(it+b);
#define CB1(a)   *(it+a)=data##a;
#define CB2(a,b) *(it+a)=data##a;*(it+b)=data##b;
  DD2(1,2) SORT2(1,2)
  DD2(4,5) SORT2(4,5)
  DD1(0)   SORT2(0,2)
  DD1(3)   SORT2(3,5)
  SORT2(0,1) SORT2(3,4) SORT2(2,5) CB1(5)
  SORT2(1,4) SORT2(0,3) CB1(0)
  SORT2(2,4) CB1(4)
  SORT2(1,3) CB1(1)
  SORT2(2,3) CB2(2,3)
#undef CB1
#undef CB2
#undef DD1
#undef DD2
#undef SORT2
}

Oppure, se si desidera passare le variabili per riferimento, utilizzare questo (la funzione seguente differisce da quella sopra nelle prime 5 righe):

template<class T> inline void sort6(T& e0, T& e1, T& e2, T& e3, T& e4, T& e5) {
#define SORT2(x,y) {if(data##x>data##y)std::swap(data##x,data##y);}
#define DD1(a)   register auto data##a=e##a;
#define DD2(a,b) register auto data##a=e##a;register auto data##b=e##b;
#define CB1(a)   e##a=data##a;
#define CB2(a,b) e##a=data##a;e##b=data##b;
  DD2(1,2) SORT2(1,2)
  DD2(4,5) SORT2(4,5)
  DD1(0)   SORT2(0,2)
  DD1(3)   SORT2(3,5)
  SORT2(0,1) SORT2(3,4) SORT2(2,5) CB1(5)
  SORT2(1,4) SORT2(0,3) CB1(0)
  SORT2(2,4) CB1(4)
  SORT2(1,3) CB1(1)
  SORT2(2,3) CB2(2,3)
#undef CB1
#undef CB2
#undef DD1
#undef DD2
#undef SORT2
}

Il motivo per utilizzare la registerparola chiave è perché questa è una delle poche volte in cui sai di volere questi valori nei registri. Senza register, il compilatore lo capirà per la maggior parte del tempo, ma a volte no. L'uso della registerparola chiave aiuta a risolvere questo problema. Normalmente, tuttavia, non utilizzare la registerparola chiave poiché è più probabile che rallenti il ​​codice piuttosto che accelerarlo.

Inoltre, notare l'uso di modelli. Questo viene fatto apposta poiché, anche con la inlineparola chiave, le funzioni dei template sono generalmente ottimizzate in modo molto più aggressivo da gcc rispetto alle funzioni C di vaniglia (ciò ha a che fare con gcc che ha bisogno di occuparsi di puntatori di funzioni per funzioni di C vanilla ma non di funzioni di template).

  1. Durante il cronometraggio di varie funzioni di ordinamento ho notato che il contesto (ovvero il codice circostante) in cui è stata effettuata la chiamata alla funzione di ordinamento ha avuto un impatto significativo sulle prestazioni, il che è probabilmente dovuto al fatto che la funzione è stata incorporata e quindi ottimizzata. Ad esempio, se il programma era sufficientemente semplice, di solito non c'era molta differenza nelle prestazioni tra il passaggio della funzione di ordinamento a un puntatore rispetto al passaggio a un iteratore; altrimenti l'uso di iteratori di solito portava a prestazioni notevolmente migliori e mai (almeno nella mia esperienza finora) prestazioni notevolmente peggiori. Sospetto che ciò possa essere dovuto al fatto che g ++ è in grado di ottimizzare globalmente un codice sufficientemente semplice.

0

Prova a "unire l'elenco ordinato". :) Usa due array. Il più veloce per array piccoli e grandi.
Se concedi, controlli solo dove inserire. Altri valori più grandi non è necessario confrontare (cmp = ab> 0).
Per 4 numeri, è possibile utilizzare il sistema 4-5 cmp (~ 4.6) o 3-6 cmp (~ 4.9). L'ordinamento delle bolle usa 6 cmp (6). Un sacco di cmp per codice più grande numeri più lenti.
Questo codice utilizza 5 cmp (non ordinamento MSL):
if (cmp(arr[n][i+0],arr[n][i+1])>0) {swap(n,i+0,i+1);} if (cmp(arr[n][i+2],arr[n][i+3])>0) {swap(n,i+2,i+3);} if (cmp(arr[n][i+0],arr[n][i+2])>0) {swap(n,i+0,i+2);} if (cmp(arr[n][i+1],arr[n][i+3])>0) {swap(n,i+1,i+3);} if (cmp(arr[n][i+1],arr[n][i+2])>0) {swap(n,i+1,i+2);}

MSL principale 9 8 7 6 5 4 3 2 1 0 89 67 45 23 01 ... concat two sorted lists, list length = 1 6789 2345 01 ... concat two sorted lists, list length = 2 23456789 01 ... concat two sorted lists, list length = 4 0123456789 ... concat two sorted lists, list length = 8

codice js

function sortListMerge_2a(cmp)	
{
var step, stepmax, tmp, a,b,c, i,j,k, m,n, cycles;
var start = 0;
var end   = arr_count;
//var str = '';
cycles = 0;
if (end>3)
	{
	stepmax = ((end - start + 1) >> 1) << 1;
	m = 1;
	n = 2;
	for (step=1;step<stepmax;step<<=1)	//bounds 1-1, 2-2, 4-4, 8-8...
		{
		a = start;
		while (a<end)
			{
			b = a + step;
			c = a + step + step;
			b = b<end ? b : end;
			c = c<end ? c : end;
			i = a;
			j = b;
			k = i;
			while (i<b && j<c)
				{
				if (cmp(arr[m][i],arr[m][j])>0)
					{arr[n][k] = arr[m][j]; j++; k++;}
				else	{arr[n][k] = arr[m][i]; i++; k++;}
				}
			while (i<b)
				{arr[n][k] = arr[m][i]; i++; k++;
}
			while (j<c)
				{arr[n][k] = arr[m][j]; j++; k++;
}
			a = c;
			}
		tmp = m; m = n; n = tmp;
		}
	return m;
	}
else
	{
	// sort 3 items
	sort10(cmp);
	return m;
	}
}


0

Ordina 4 elementi con utilizzo cmp == 0. Il numero di cmp è ~ 4,34 (il nativo FF ha ~ 4,52), ma impiega 3 volte il tempo rispetto alle liste di fusione. Meglio meno operazioni cmp, se si hanno numeri grandi o testo grande. Modifica: bug riparato

Test online http://mlich.zam.slu.cz/js-sort/x-sort-x2.htm

function sort4DG(cmp,start,end,n) // sort 4
{
var n     = typeof(n)    !=='undefined' ? n   : 1;
var cmp   = typeof(cmp)  !=='undefined' ? cmp   : sortCompare2;
var start = typeof(start)!=='undefined' ? start : 0;
var end   = typeof(end)  !=='undefined' ? end   : arr[n].length;
var count = end - start;
var pos = -1;
var i = start;
var cc = [];
// stabilni?
cc[01] = cmp(arr[n][i+0],arr[n][i+1]);
cc[23] = cmp(arr[n][i+2],arr[n][i+3]);
if (cc[01]>0) {swap(n,i+0,i+1);}
if (cc[23]>0) {swap(n,i+2,i+3);}
cc[12] = cmp(arr[n][i+1],arr[n][i+2]);
if (!(cc[12]>0)) {return n;}
cc[02] = cc[01]==0 ? cc[12] : cmp(arr[n][i+0],arr[n][i+2]);
if (cc[02]>0)
    {
    swap(n,i+1,i+2); swap(n,i+0,i+1); // bubble last to top
    cc[13] = cc[23]==0 ? cc[12] : cmp(arr[n][i+1],arr[n][i+3]);
    if (cc[13]>0)
        {
        swap(n,i+2,i+3); swap(n,i+1,i+2); // bubble
        return n;
        }
    else    {
    cc[23] = cc[23]==0 ? cc[12] : (cc[01]==0 ? cc[30] : cmp(arr[n][i+2],arr[n][i+3]));  // new cc23 | c03 //repaired
        if (cc[23]>0)
            {
            swap(n,i+2,i+3);
            return n;
            }
        return n;
        }
    }
else    {
    if (cc[12]>0)
        {
        swap(n,i+1,i+2);
        cc[23] = cc[23]==0 ? cc[12] : cmp(arr[n][i+2],arr[n][i+3]); // new cc23
        if (cc[23]>0)
            {
            swap(n,i+2,i+3);
            return n;
            }
        return n;
        }
    else    {
        return n;
        }
    }
return n;
}

1
Il caso d'uso è leggermente diverso dal contesto iniziale della domanda. Con ordinamenti a lunghezza fissa i dettagli contano e il conteggio dei cmp di swap non è sufficiente. Non sarei nemmeno sorpreso se non fosse il tipo reale che consumerebbe tempo, ma qualcosa di completamente diverso che chiama typeof () nell'init. Non so come eseguire la misurazione dell'ora effettiva utilizzando Javascript. Forse con nodo?
Kriss,

0

Forse sono in ritardo alla festa, ma almeno il mio contributo è un nuovo approccio.

  • Il codice dovrebbe davvero essere integrato
  • anche se in linea, ci sono troppi rami
  • la parte di analisi è sostanzialmente O (N (N-1)) che sembra OK per N = 6
  • il codice potrebbe essere più efficace se il costo diswap fosse maggiore (irt il costo di compare)
  • Confido che le funzioni statiche vengano incorporate.
  • Il metodo è correlato all'ordinamento per gradi
    • anziché i ranghi, vengono utilizzati i gradi relativi (offset).
    • la somma dei ranghi è zero per ogni ciclo in qualsiasi gruppo di permutazione.
    • invece di SWAP()ing due elementi, i cicli vengono inseguiti, richiedendo solo una temperatura e uno scambio (registro-> registro) (nuovo <- vecchio).

Aggiornamento: modificato un po 'il codice, alcune persone usano i compilatori C ++ per compilare il codice C ...

#include <stdio.h>

#if WANT_CHAR
typedef signed char Dif;
#else
typedef signed int Dif;
#endif

static int walksort (int *arr, int cnt);
static void countdifs (int *arr, Dif *dif, int cnt);
static void calcranks(int *arr, Dif *dif);

int wsort6(int *arr);

void do_print_a(char *msg, int *arr, unsigned cnt)
{
fprintf(stderr,"%s:", msg);
for (; cnt--; arr++) {
        fprintf(stderr, " %3d", *arr);
        }
fprintf(stderr,"\n");
}

void do_print_d(char *msg, Dif *arr, unsigned cnt)
{
fprintf(stderr,"%s:", msg);
for (; cnt--; arr++) {
        fprintf(stderr, " %3d", (int) *arr);
        }
fprintf(stderr,"\n");
}

static void inline countdifs (int *arr, Dif *dif, int cnt)
{
int top, bot;

for (top = 0; top < cnt; top++ ) {
        for (bot = 0; bot < top; bot++ ) {
                if (arr[top] < arr[bot]) { dif[top]--; dif[bot]++; }
                }
        }
return ;
}
        /* Copied from RexKerr ... */
static void inline calcranks(int *arr, Dif *dif){

dif[0] =     (arr[0]>arr[1])+(arr[0]>arr[2])+(arr[0]>arr[3])+(arr[0]>arr[4])+(arr[0]>arr[5]);
dif[1] = -1+ (arr[1]>=arr[0])+(arr[1]>arr[2])+(arr[1]>arr[3])+(arr[1]>arr[4])+(arr[1]>arr[5]);
dif[2] = -2+ (arr[2]>=arr[0])+(arr[2]>=arr[1])+(arr[2]>arr[3])+(arr[2]>arr[4])+(arr[2]>arr[5]);
dif[3] = -3+ (arr[3]>=arr[0])+(arr[3]>=arr[1])+(arr[3]>=arr[2])+(arr[3]>arr[4])+(arr[3]>arr[5]);
dif[4] = -4+ (arr[4]>=arr[0])+(arr[4]>=arr[1])+(arr[4]>=arr[2])+(arr[4]>=arr[3])+(arr[4]>arr[5]);
dif[5] = -(dif[0]+dif[1]+dif[2]+dif[3]+dif[4]);
}

static int walksort (int *arr, int cnt)
{
int idx, src,dst, nswap;

Dif difs[cnt];

#if WANT_REXK
calcranks(arr, difs);
#else
for (idx=0; idx < cnt; idx++) difs[idx] =0;
countdifs(arr, difs, cnt);
#endif
calcranks(arr, difs);

#define DUMP_IT 0
#if DUMP_IT
do_print_d("ISteps ", difs, cnt);
#endif

nswap = 0;
for (idx=0; idx < cnt; idx++) {
        int newval;
        int step,cyc;
        if ( !difs[idx] ) continue;
        newval = arr[idx];
        cyc = 0;
        src = idx;
        do      {
                int oldval;
                step = difs[src];
                difs[src] =0;
                dst = src + step;
                cyc += step ;
                if(dst == idx+1)idx=dst;
                oldval = arr[dst];
#if (DUMP_IT&1)
                fprintf(stderr, "[Nswap=%d] Cyc=%d Step=%2d Idx=%d  Old=%2d New=%2d #### Src=%d Dst=%d[%2d]->%2d <-- %d\n##\n"
                        , nswap, cyc, step, idx, oldval, newval
                        , src, dst, difs[dst], arr[dst]
                        , newval  );
                do_print_a("Array ", arr, cnt);
                do_print_d("Steps ", difs, cnt);
#endif

                arr[dst] = newval;
                newval = oldval;
                nswap++;
                src = dst;
                } while( cyc);
        }

return nswap;
}
/*************/
int wsort6(int *arr)
{
return walksort(arr, 6);
}

sembra una specie di bolla. Potenzialmente un buon concorrente per l'implementazione più lenta, ma può essere ancora interessante sapere se lavorare sul codice fa molta differenza. Inserisci il tuo codice nello stesso formato degli altri, in modo da poter eseguire il benchmark su di esso.
Kriss,

@kriss en.wikipedia.org/wiki/Permutation_group È certamente non bubble sort: il codice rileva cicli nel dato permutazione, e cammina questi cicli, mettendo ogni elemento al suo posto finale. La wsort6()funzione finale ha l'interfaccia corretta.
Joop,

@joop: il mio cattivo, nessun tipo di bolla davvero. Detto questo nel contesto, mi aspetto ancora che il codice sia molto peggio di qualsiasi altra implementazione corrente. A proposito, la soluzione Order Rank è ottimale per quanto riguarda il numero di swap in quanto trova direttamente la posizione finale di ogni articolo. Non è anche chiaro se walksort funziona anche quando rimuoviamo l'ipotesi che tutti i numeri ordinati siano diversi come qui. Per confrontare il codice dovremmo usare il codice di traccia. Inoltre, dato che di solito sto compilando su un compilatore C ++, il codice non funzionerà perché l'OP ha chiamato una variabile "new" (e questo interrompe l'evidenziazione della sintassi).
Kriss,

Il metodo è molto vicino all'ordine di rango, solo le assegnazioni finali vengono eseguite sul posto . A parte i ranghi o1..o5, non è necessario il secondo e[6]array temporaneo . E: compilare il codice C su un compilatore C ++ e dare la colpa al codice?
Joop,

@greybeard: grazie, ho aggiunto uno spazio prima #include. Risolto
wildplasser

0
//Bruteforce compute unrolled count dumbsort(min to 0-index)
void bcudc_sort6(int* a)
{
    int t[6] = {0};
    int r1,r2;

    r1=0;
    r1 += (a[0] > a[1]);
    r1 += (a[0] > a[2]);
    r1 += (a[0] > a[3]);
    r1 += (a[0] > a[4]);
    r1 += (a[0] > a[5]);
    while(t[r1]){r1++;}
    t[r1] = a[0];

    r2=0;
    r2 += (a[1] > a[0]);
    r2 += (a[1] > a[2]);
    r2 += (a[1] > a[3]);
    r2 += (a[1] > a[4]);
    r2 += (a[1] > a[5]);
    while(t[r2]){r2++;} 
    t[r2] = a[1];

    r1=0;
    r1 += (a[2] > a[0]);
    r1 += (a[2] > a[1]);
    r1 += (a[2] > a[3]);
    r1 += (a[2] > a[4]);
    r1 += (a[2] > a[5]);
    while(t[r1]){r1++;}
    t[r1] = a[2];

    r2=0;
    r2 += (a[3] > a[0]);
    r2 += (a[3] > a[1]);
    r2 += (a[3] > a[2]);
    r2 += (a[3] > a[4]);
    r2 += (a[3] > a[5]);
    while(t[r2]){r2++;} 
    t[r2] = a[3];

    r1=0;
    r1 += (a[4] > a[0]);
    r1 += (a[4] > a[1]);
    r1 += (a[4] > a[2]);
    r1 += (a[4] > a[3]);
    r1 += (a[4] > a[5]);
    while(t[r1]){r1++;}
    t[r1] = a[4];

    r2=0;
    r2 += (a[5] > a[0]);
    r2 += (a[5] > a[1]);
    r2 += (a[5] > a[2]);
    r2 += (a[5] > a[3]);
    r2 += (a[5] > a[4]);
    while(t[r2]){r2++;} 
    t[r2] = a[5];

    a[0]=t[0];
    a[1]=t[1];
    a[2]=t[2];
    a[3]=t[3];
    a[4]=t[4];
    a[5]=t[5];
}

static __inline__ void sort6(int* a)
{
    #define wire(x,y); t = a[x] ^ a[y] ^ ( (a[x] ^ a[y]) & -(a[x] < a[y]) ); a[x] = a[x] ^ t; a[y] = a[y] ^ t;
    register int t;

    wire( 0, 1); wire( 2, 3); wire( 4, 5);
    wire( 3, 5); wire( 0, 2); wire( 1, 4);
    wire( 4, 5); wire( 2, 3); wire( 0, 1); 
    wire( 3, 4); wire( 1, 2); 
    wire( 2, 3);

    #undef wire
}

Indipendentemente dalla velocità, sei sicuro che funzioni? In bruteforce sort i tuoi loop sono dubbi. Mi sembra che non funzioneranno se abbiamo uno zero in valori ordinati.
Kriss,

1
t [6] l'array è inizializzato su 0x0. Quindi non importa dove e se verrà scritta una chiave valutata 0x0.
Dal

-1

Bene, se sono solo 6 elementi e puoi sfruttare il parallelismo, vuoi minimizzare la ramificazione condizionale, ecc. Perché non generi tutte le combinazioni e testiamo l'ordine? Mi permetto che in alcune architetture, può essere piuttosto veloce (a patto che tu abbia la memoria preallocata)


9
Ci sono 720 ordini e le versioni veloci sono sotto i 100 cicli. Anche se il parallelismo massiccio potesse essere sfruttato, a una scala temporale così ridotta il costo di creazione e sincronizzazione dei thread probabilmente supererebbe il costo del semplice ordinamento degli array su un core.
Kevin Stock

-3

Ecco tre metodi di ordinamento tipici che rappresentano tre diverse classi di algoritmi di ordinamento:

Insertion Sort: Θ(n^2)

Heap Sort: Θ(n log n)

Count Sort: Θ(3n)

Ma dai un'occhiata alla discussione di Stefan Nelsson sull'algoritmo di ordinamento più veloce? dove discute una soluzione che va fino a O(n log log n).. verificarne l'implementazione in C

Questo algoritmo di ordinamento semi-lineare è stato presentato da un documento nel 1995:

A. Andersson, T. Hagerup, S. Nilsson e R. Raman. Ordinamento in tempo lineare? In Atti del 27 ° Simposio annuale ACM sulla teoria dell'informatica, pagine 427-436, 1995.


8
Questo è interessante ma a parte questo. Big-Θ ha lo scopo di nascondere i fattori costanti e mostrare la tendenza quando la dimensione del problema (n) aumenta. Il problema qui riguarda completamente una dimensione del problema fissa (n = 6) e tenendo conto dei fattori costanti.
Kriss,

@kriss hai ragione, il mio confronto è asintotico, quindi il confronto pratico mostrerà se è più veloce o no per quel caso
Khaled.K

4
Non puoi concludere, perché ogni diverso algoritmo nasconde una costante K moltiplicativa diversa (e anche una costante additiva C). ovvero: k0, c0 per ordinamento inserzione, k1, c1 per ordinamento heap e così via. Tutte queste costanti sono effettivamente diverse (si potrebbe dire in termini fisici che ogni algoritmo ha il suo "coefficiente di attrito"). In questo caso non è possibile concludere che un algoritmo sia effettivamente più veloce (o qualsiasi n caso fisso).
Kriss,
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.