web-dev-qa-db-fra.com

Sorte la plus rapide de la matrice fixe 6 int

Répondant à une autre question de débordement de pile ( celle-ci ), je suis tombé sur un sous-problème intéressant. Quel est le moyen le plus rapide de trier un tableau de 6 entiers?

Comme la question est très basse:

  • nous ne pouvons pas supposer que les bibliothèques sont disponibles (et l'appel a son coût), seulement C simple
  • pour éviter de vider le pipeline d’instructions (qui a un coût élevé very ), nous devrions probablement minimiser les branches, les sauts et tout autre type de rupture de flux de contrôle (comme ceux cachés derrière des points de séquence dans && ou ||).
  • la place est restreinte et la minimisation des registres et de l'utilisation de la mémoire est un problème; idéalement, le tri sur place est probablement préférable.

Vraiment cette question est une sorte de Golf où l’objectif n’est pas de minimiser la longueur de la source mais le temps d’exécution. Je l'appelle le code 'Zening' tel qu'il est utilisé dans le titre du livre optimisation du code zen par Michael Abrash et ses suites .

Pourquoi est-ce intéressant? Il y a plusieurs couches:

  • l'exemple est simple et facile à comprendre et à mesurer, peu de compétences C impliquées
  • il montre les effets du choix d'un bon algorithme pour le problème, mais aussi ceux du compilateur et du matériel sous-jacent.

Voici ma mise en œuvre de référence (naïve, non optimisée) et mon ensemble de tests.

#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);
}

Résultats bruts

Le nombre de variantes devenant de plus en plus important, je les ai toutes rassemblées dans une suite de tests que l'on peut trouver ici . Les tests utilisés sont un peu moins naïfs que ceux présentés ci-dessus, grâce à Kevin Stock. Vous pouvez le compiler et l'exécuter dans votre propre environnement. Je suis assez intéressé par le comportement de différents architectes/compilateurs cibles. (OK les gars, mettez-le dans les réponses, je vais +1 chaque contributeur d'un nouvel ensemble de résultats).

J'ai donné la réponse à Daniel Stutzbach (pour le golf) il y a un an, car il était à la source de la solution la plus rapide à cette époque (réseaux de tri).

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

  • Appel direct à la fonction de bibliothèque qsort: 689.38
  • Mise en oeuvre naïve (sorte d'insertion): 285.70
  • Tri d'insertion (Daniel Stutzbach): 142.12
  • Tri d'insertion non déroulé: 125.47
  • Ordre de classement: 102.26
  • Classement Ordre avec les registres: 58.03
  • Réseaux de tri (Daniel Stutzbach): 111.68
  • Réseaux de tri (Paul R): 66.36
  • Tri des réseaux 12 avec échange rapide: 58,86
  • Tri des réseaux 12 échanges commandés: 53.74
  • Tri des réseaux 12 nouvelle commande simple Swap: 31.54
  • Réseau de tri réorganisé avec échange rapide: 31.54
  • Réseau de tri réorganisé avec échange rapide V2: 33.63
  • Tri de bulles en ligne (Paolo Bonzini): 48.85
  • Tri d'insertion déroulée (Paolo Bonzini): 75.30

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

  • Appel direct à la fonction de bibliothèque qsort: 705.93
  • Implémentation naïve (type d'insertion): 135.60
  • Tri d'insertion (Daniel Stutzbach): 142.11
  • Tri d'insertion non déroulé: 126.75
  • Classement: 46.42
  • Classement Ordre avec les registres: 43.58
  • Réseaux de tri (Daniel Stutzbach): 115.57
  • Réseaux de tri (Paul R): 64.44
  • Tri des réseaux 12 avec échange rapide: 61,98
  • Tri des réseaux 12 échange ordonné: 54,67
  • Tri des réseaux 12 nouvelle commande simple Swap: 31.54
  • Réseau de tri réorganisé avec échange rapide: 31.24
  • Réseau de tri réorganisé avec échange rapide V2: 33.07
  • Tri à bulles en ligne (Paolo Bonzini): 45.79
  • Tri d'insertion déroulée (Paolo Bonzini): 80.15

J'ai inclus les résultats -O1 et -O2 car, de manière surprenante, pour plusieurs programmes, O2 est moins efficace que O1. Je me demande quelle optimisation spécifique a cet effet?

Commentaires sur les solutions proposées

Tri par insertion (Daniel Stutzbach)

Comme prévu, minimiser les branches est en effet une bonne idée.

Réseaux de tri (Daniel Stutzbach)

Mieux que le tri par insertion. Je me demandais si l’effet principal n’était pas d’éviter la boucle externe. Je l’ai essayé en effectuant une sorte d’insertion déroulée pour vérifier et nous obtenons à peu près les mêmes chiffres (le code est ici ).

Réseaux de tri (Paul R)

Le meilleur jusqu'ici. Le code que j'ai utilisé pour tester est here . Je ne sais pas encore pourquoi il est presque deux fois plus rapide que l’autre implémentation du réseau de tri. Paramètre passant? Fast max?

Tri des réseaux 12 SWAP avec échange rapide

Comme suggéré par Daniel Stutzbach, j’ai combiné son réseau de tri à 12 échanges avec un échange rapide sans branche (le code est ici ). Il est en effet plus rapide, le meilleur à ce jour avec une petite marge (environ 5%) comme on pourrait s’y attendre avec 1 swap de moins.

Il est également intéressant de noter que l’échange sans embranchement semble être beaucoup (4 fois) moins efficace que le simple qui utilise l’architecture si PPC.

Appel de la bibliothèque qsort

Pour donner un autre point de référence, j'ai également essayé, comme suggéré, d'appeler simplement la bibliothèque qsort (le code est ici ). Comme prévu, il est beaucoup plus lent: de 10 à 30 fois plus lent ... comme cela est devenu évident avec la nouvelle suite de tests, le principal problème semble être le chargement initial de la bibliothèque après le premier appel, et il ne se compare pas si mal avec les autres version. Il est juste entre 3 et 20 fois plus lent sur mon Linux. Sur certaines architectures utilisées pour des tests par d’autres, cela semble même être plus rapide (je suis vraiment surpris par celle-là, car la bibliothèque qsort utilise une API plus complexe).

Ordre de classement

Rex Kerr a proposé une autre méthode complètement différente: pour chaque élément du tableau, calculez directement sa position finale. Ceci est efficace car l'ordre de classement informatique n'a pas besoin de branche. L'inconvénient de cette méthode est qu'il faut trois fois plus de mémoire que le tableau (une copie du tableau et des variables pour stocker les ordres de classement). Les résultats sont très surprenants (et intéressants). Sur mon architecture de référence avec système d'exploitation 32 bits et Intel Core2 Quad E8300, le nombre de cycles était légèrement inférieur à 1 000 (tout comme le tri des réseaux avec permutation de branches). Mais une fois compilé et exécuté sur ma boîte 64 bits (Intel Core2 Duo), il fonctionnait beaucoup mieux: il est devenu le plus rapide à ce jour. J'ai finalement découvert la vraie raison. Ma boîte de 32 bits utilise gcc 4.4.1 et ma boîte de 64 bits gcc 4.4.3 et la dernière semble beaucoup mieux pour optimiser ce code particulier (il y avait très peu de différence pour les autres propositions).

update :

Comme le montrent les chiffres publiés ci-dessus, cet effet était encore renforcé par les versions ultérieures de gcc et Rank Order devenait systématiquement deux fois plus rapide que toute autre alternative.

Tri des réseaux 12 avec permutation réorganisée

L'incroyable efficacité de la proposition de Rex Kerr avec gcc 4.4.3 m'a fait me demander: comment un programme utilisant 3 fois plus de mémoire peut-il être plus rapide qu'un réseau de tri sans embranchement? Mon hypothèse était qu'il y avait moins de dépendances du type lecture après écriture, ce qui permettait une meilleure utilisation du programmateur d'instructions superscalaires du x86. Cela m'a donné une idée: réorganiser les échanges pour minimiser les dépendances lecture après écriture. Plus simplement: quand vous faites SWAP(1, 2); SWAP(0, 2);, vous devez attendre la fin du premier échange avant d’effectuer le second, car les deux accèdent à une cellule de mémoire commune. Quand vous faites SWAP(1, 2); SWAP(4, 5);, le processeur peut exécuter les deux en parallèle. Je l'ai essayé et cela fonctionne comme prévu, le tri des réseaux est environ 10% plus rapide.

Tri des réseaux 12 avec échange simple

Un an après le post original, Steinar H. Gunderson a suggéré de ne pas essayer de déjouer le compilateur et de garder le code d'échange simple. C'est en effet une bonne idée car le code résultant est environ 40% plus rapide! Il a également proposé un échange optimisé manuellement utilisant un code d'assemblage en ligne x86 qui peut encore épargner encore plus de cycles. Le plus surprenant (il en dit long sur la psychologie du programmeur) est qu’aucun d’entre eux n’a essayé cette version du swap. Le code que j'avais l'habitude de tester est ici . D'autres ont suggéré d'autres manières d'écrire un échange rapide en C, mais cela donne les mêmes performances que le simple avec un compilateur décent.

Le "meilleur" code est maintenant le suivant:

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
}

Si nous croyons que notre ensemble de tests (et, oui, il est assez médiocre, c’est simplement un avantage, c’est d’être court, simple et facile à comprendre ce que nous mesurons), le nombre moyen de cycles du code résultant pour un type est inférieur à 40 cycles ( 6 tests sont exécutés). Cela met chaque échange à une moyenne de 4 cycles. J'appelle ça incroyablement vite. D'autres améliorations possibles?

391
kriss

Pour toute optimisation, il est toujours préférable de tester, tester, tester. J'essayerais au moins le tri des réseaux et le tri par insertion. Si je pariais, je placerais mon argent dans une sorte d'insertion basée sur l'expérience passée.

Savez-vous quelque chose sur les données d'entrée? Certains algorithmes fonctionneront mieux avec certains types de données. Par exemple, le tri par insertion donne de meilleurs résultats avec les données triées ou presque triées. Ce sera donc le meilleur choix si les chances de tri des données sont presque supérieures à la moyenne.

L'algorithme que vous avez posté est similaire à un tri par insertion, mais il semble que vous ayez réduit le nombre de swaps au prix de comparaisons supplémentaires. Les comparaisons coûtent toutefois beaucoup plus cher que les échanges, car les branches peuvent provoquer le blocage du pipeline d'instructions.

Voici une implémentation du type insertion:

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;
        }
}

Voici comment je construirais un réseau de tri. Tout d’abord, utilisez this site pour générer un ensemble minimal de macros SWAP pour un réseau de la longueur appropriée. Envelopper cela dans une fonction me donne:

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
}
159

Voici une implémentation utilisant réseaux de tri :

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);  
}

Pour cela, vous avez vraiment besoin d’implémentations min et max sans branches très efficaces, car c’est effectivement ce que ce code revient à - une séquence d’opérations min et max (13 sur chacun au total). Je laisse cela comme un exercice pour le lecteur.

Notez que cette implémentation se prête facilement à la vectorisation (par exemple, SIMD - la plupart des ISA SIMD ont des instructions de vecteur min/max) ainsi que pour les implémentations GPU (par exemple, CUDA - étant dépourvues de branches, la divergence de distorsion ne pose aucun problème).

Voir aussi: Implémentation d'algorithme rapide pour trier une très petite liste

60
Paul R

Puisque ce sont des entiers et que les comparaisons sont rapides, pourquoi ne pas calculer directement le rang de chacun:

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];
}
44
Rex Kerr

On dirait que je suis arrivé à la fête avec un an de retard, mais c'est parti ...

En regardant l'assemblage généré par gcc 4.5.2, j'ai observé que des chargements et des magasins sont effectués pour chaque échange, ce qui n'est vraiment pas nécessaire. Il serait préférable de charger les 6 valeurs dans des registres, de les trier et de les stocker dans la mémoire. J'ai ordonné que les chargements dans les magasins soient aussi proches que possible de là. Les registres sont d'abord nécessaires et utilisés en dernier. J'ai également utilisé la macro SWAP de Steinar H. Gunderson. Mise à jour: je suis passé à la macro SWAP de Paolo Bonzini, que gcc convertit en quelque chose de similaire à celle de Gunderson, mais gcc est capable de mieux ordonner les instructions car elles ne sont pas données en tant qu'assembly explicite.

J'ai utilisé le même ordre d'échange que le réseau d'échange réorganisé, considéré comme le plus performant, bien qu'il puisse y avoir un meilleur ordre. Si je trouve un peu plus de temps, je générerai et testerai un tas de permutations.

J'ai changé le code de test pour prendre en compte plus de 4000 matrices et afficher le nombre moyen de cycles nécessaires pour les trier. Sur un i5-650, je reçois environ 34,1 cycles/tri (avec -O3), alors que le réseau de tri réorganisé d'origine obtenait environ 65,3 cycles/tri (avec -O1, battements -O2 et -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;
}

J'ai changé modifié la suite de tests pour signaler également les horloges par tri et exécuter plus de tests (la fonction cmp a été mise à jour pour gérer également le dépassement d'entier), voici les résultats sur différentes architectures. J'ai essayé de tester sur un processeur AMD mais rdtsc n'est pas fiable sur le X6 1100T que j'ai disponible.

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
35
Kevin Stock

Je suis tombé sur cette question posée par Google il y a quelques jours, car j'avais également besoin de trier rapidement un tableau de longueur fixe de 6 nombres entiers. Dans mon cas cependant, mes nombres entiers ne sont que 8 bits (au lieu de 32) et je n’ai pas une obligation stricte d’utiliser uniquement C. Je pensais partager mes résultats de toute façon, au cas où ils pourraient être utiles à quelqu'un ...

J'ai implémenté une variante d'un tri de réseau dans Assembly qui utilise SSE pour vectoriser les opérations de comparaison et d'échange dans la mesure du possible. Il faut six "passes" pour trier complètement le tableau. J'ai utilisé un nouveau mécanisme pour convertir directement les résultats de PCMPGTB (comparaison vectorisée) en paramètres shuffle pour PSHUFB (échange vectorisé), en utilisant uniquement une instruction PADDB (addition vectorisée) et, dans certains cas, une instruction PAND (ET au niveau du bit).

Cette approche a également eu l’effet secondaire de produire une fonction véritablement sans branches. Il n'y a aucune instruction de saut que ce soit.

Il semble que cette implémentation soit environ 38% plus rapide que l’implémentation actuellement marquée comme étant l’option la plus rapide de la question ("Tri des réseaux 12 avec un échange simple"). "). J'ai modifié cette implémentation pour utiliser char éléments de tableau lors de mes tests, afin de rendre la comparaison équitable.

Je devrais noter que cette approche peut être appliquée à n'importe quelle taille de tableau jusqu'à 16 éléments. Je m'attends à ce que l’avantage de la vitesse relative par rapport aux solutions de remplacement s’agrandisse pour les baies plus grandes.

Le code est écrit en MASM pour les processeurs x86_64 avec SSSE3. La fonction utilise la "nouvelle" convention d'appel Windows x64. C'est ici...

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

Vous pouvez le compiler en un objet exécutable et le lier à votre projet C. Pour savoir comment procéder dans Visual Studio, vous pouvez lire cet article . Vous pouvez utiliser le prototype C suivant pour appeler la fonction à partir de votre code C:

void simd_sort_6(char *values);
15
Joe Crivello

Le code de test est assez mauvais; il déborde du tableau initial (les gens ici ne lisent-ils pas les avertissements du compilateur?), le printf imprime les mauvais éléments, il utilise .byte pour rdtsc sans raison valable, il n’ya qu’une exécution (!), rien ne vérifie que le les résultats finaux sont en fait corrects (il est donc très facile d’optimiser pour obtenir quelque chose de subtilement faux), les tests inclus sont très rudimentaires (pas de nombres négatifs?) et rien n’empêche le compilateur d’écarter simplement la fonction entière en tant que code mort.

Cela étant dit, il est également assez facile d'améliorer la solution de réseau bitonique; changez simplement les commandes min/max/SWAP en

#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"); }

et il est environ 65% plus rapide pour moi (Debian gcc 4.4.5 avec -O2, AMD64, Core i7).

14

Bien que j'aime beaucoup la macro d'échange fournie:

#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; }

Je vois une amélioration (qu'un bon compilateur pourrait apporter):

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

Nous prenons note du fonctionnement de min et max et tirons explicitement la sous-expression commune. Cela élimine complètement les macros min et max.

13
phkahler

N'optimisez jamais les valeurs min/max sans procéder à des analyses comparatives et ne pas regarder l'assembly généré par le compilateur Si je laisse GCC optimiser le minimum avec des instructions de déplacement conditionnelles, j'obtiens une accélération de 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 cycles dans le code de test). Faire max avec?: Est plus ou moins la même chose, presque perdu dans le bruit, mais ce qui précède est un peu plus rapide. Ce SWAP est plus rapide avec GCC et Clang.

Les compilateurs font également un travail exceptionnel en matière d’allocation de registre et d’analyse de pseudonymes: ils déplacent effectivement d [x] dans des variables locales à l’avance et ne les copient que dans la mémoire à la fin. En fait, ils le font même mieux que si vous travailliez entièrement avec des variables locales (comme d0 = d[0], d1 = d[1], d2 = d[2], d3 = d[3], d4 = d[4], d5 = d[5]). J'écris ceci parce que vous supposez une forte optimisation et essayez pourtant de déjouer le compilateur sur min/max. :)

Au fait, j'ai essayé Clang et GCC. Ils font la même optimisation, mais à cause des différences d’horaire, les résultats ont des variations, ils ne peuvent pas vraiment dire lequel est le plus rapide ou le plus lent. GCC est plus rapide sur les réseaux de tri, Clang sur les tris quadratiques.

Juste pour être complet, le tri à bulles déroulé et le tri à insertion sont également possibles. Voici le genre de bulle:

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);

et voici le type d'insertion:

//#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);

Ce type d'insertion est plus rapide que celui de Daniel Stutzbach et convient particulièrement bien à un GPU ou à un ordinateur avec prédication, car ITER peut être exécuté avec seulement 3 instructions (contre 4 pour SWAP). Par exemple, voici la ligne t = d[2]; ITER(1); ITER(0); dans ARM Assembly:

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

Pour six éléments, le tri par insertion est compétitif par rapport au réseau de tri (12 swaps contre 15 itérations: 4 instructions/swap contre 3 instructions/itération); le type de bulle est bien sûr plus lent. Mais ce ne sera pas vrai quand la taille augmentera, puisque le tri par insertion est O (n ^ 2) alors que les réseaux de tri sont O (n log n).

12
Paolo Bonzini

J'ai porté la suite de tests sur une machine PPC que je n'arrive pas à identifier (je n'ai pas eu à toucher au code, mais simplement à augmenter le nombre d'itérations du test, à utiliser 8 scénarios de test pour éviter de polluer les résultats avec des mods et à remplacer le code. rdtsc spécifique x86):

appel direct à la fonction de bibliothèque qsort: 101

implémentation naïve (type d'insertion): 299

Tri d'insertion (Daniel Stutzbach): 108

Insertion Sort Unrolled: 51

Réseaux de tri (Daniel Stutzbach): 26

Réseaux de tri (Paul R): 85

Tri des réseaux 12 avec échange rapide: 117

Tri des réseaux 12 permutations réorganisées: 116

classement: 56

11
jheriko

Un échange XOR peut être utile dans vos fonctions de permutation.

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

Le if peut causer trop de divergence dans votre code, mais si vous avez la garantie que tous vos ints sont uniques, cela pourrait être pratique.

7
naj

J'attendais avec impatience de tenter ma chance et d’apprendre de ces exemples, mais tout d’abord quelques synchronisations de mon 1,5 GHz PPC Powerbook G4 avec 1 Go de RAM DDR. (J'ai emprunté un minuteur similaire à rdtsc pour PPC de http://www.mcs.anl.gov/~kazutomo/rdtsc.html pour les timings.) le programme à quelques reprises et les résultats absolus variaient, mais le test le plus rapide consistait systématiquement en "Tri par insertion (Daniel Stutzbach)", suivi de près par "Tri par insertion".

Voici la dernière série de fois:

**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
5
Nico

Voici ma contribution à ce fil de discussion: un shellsort optimisé à 1, 4 gap pour un vecteur int à 6 membres (valp) contenant des valeurs uniques.

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);
}

Sur mon ordinateur portable HP dv7-3010so avec un Athlon M300 à 2 cœurs à double cœur (mémoire DDR2), il s’exécute en 165 cycles d’horloge. Il s'agit d'une moyenne calculée à partir du minutage de chaque séquence unique (6!/720 au total). Compilé en Win32 avec OpenWatcom 1.8. La boucle est essentiellement une sorte d'insertion et comporte 16 instructions/37 octets de long.

Je n'ai pas d'environnement 64 bits sur lequel compiler.

4
Olof Forshell

Je sais que je suis très en retard, mais je souhaitais expérimenter différentes solutions. Tout d'abord, j'ai nettoyé cette pâte, je l'ai compilée et stockée dans un référentiel. J'ai gardé certaines solutions indésirables comme impasses afin que d'autres ne l'essaient pas. Parmi celles-ci figurait ma première solution, qui visait à garantir que x1> x2 était calculé une fois. Après optimisation, il n’est pas plus rapide que les autres versions simples.

J'ai ajouté une version en boucle du tri par ordre de rang, car ma propre application de cette étude concerne le tri de 2 à 8 éléments. Par conséquent, comme il existe un nombre variable d'arguments, une boucle est nécessaire. C’est aussi la raison pour laquelle j’ai ignoré les solutions de réseau de tri.

Le code de test n'a pas vérifié que les doublons étaient gérés correctement. Par conséquent, bien que les solutions existantes soient toutes correctes, j'ai ajouté un cas particulier au code de test pour garantir que les doublons étaient gérés correctement.

Ensuite, j'ai écrit un type d'insertion entièrement dans les registres AVX. Sur ma machine, il est 25% plus rapide que les autres types d'insertion, mais 100% plus lent que le classement. Je l'ai fait uniquement à titre d'expérience et je ne m'attendais pas à ce que ce soit mieux en raison de la ramification en mode insertion.

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
}

Ensuite, j'ai écrit un tri par ordre de priorité en utilisant AVX. Cela correspond à la vitesse des autres solutions de classement, mais n’est pas plus rapide. Le problème ici est que je ne peux calculer que les indices avec AVX, puis je dois créer un tableau d’indices. En effet, le calcul est basé sur la destination plutôt que sur la source. Voir Conversion d'indices basés sur la source en indices basés sur la destination

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];
}

Le repo peut être trouvé ici: https://github.com/eyepatchParrot/sort6/

3
eyepatch

Si le type d’insertion est raisonnablement compétitif ici, je recommanderais d’essayer un shortsort. J'ai bien peur que 6 éléments soient probablement trop peu pour être parmi les meilleurs, mais cela vaut peut-être la peine d'essayer.

Exemple de code, non testé, non dépanné, etc. Vous souhaitez ajuster la séquence inc = 4 et inc - = 3 pour trouver l’optimum (essayez inc = 2, inc - = 1 par exemple).

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;
        }
    }
}

Je ne pense pas que cela va gagner, mais si quelqu'un pose une question sur le tri des 10 éléments, qui sait ...

Selon Wikipedia, cela peut même être combiné avec des réseaux de tri: Pratt, V (1979). Réseaux de shells et de tri (mémoires exceptionnels en informatique). Garland. ISBN 0-824-04406-1

3
gcp

Cette question est en train de devenir assez ancienne, mais je devais en fait résoudre le même problème ces jours-ci: des agorithmes rapides pour trier de petits tableaux. Je pensais que ce serait une bonne idée de partager mes connaissances. Alors que j’avais commencé par utiliser des réseaux de tri, j’ai finalement réussi à trouver d’autres algorithmes pour lesquels le nombre total de comparaisons effectuées pour trier chaque permutation de 6 valeurs était inférieur à celui des réseaux de tri et inférieur à celui des tris par insertion. Je n'ai pas compté le nombre de swaps; Je m'attendrais à ce qu'il soit à peu près équivalent (peut-être un peu plus haut parfois).

L'algorithme sort6 utilise l'algorithme sort4 qui utilise l'algorithme sort3. Voici l’implémentation sous une forme C++ légère (l’original étant très lourd en modèles, il peut donc fonctionner avec tout itérateur à accès aléatoire et toute fonction de comparaison appropriée).

Tri de 3 valeurs

L'algorithme suivant est une sorte d'insertion déroulée. Lorsque deux échanges (6 affectations) doivent être effectués, il utilise 4 affectations:

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]);
            }
        }
    }
}

Cela semble un peu complexe, car le type a plus ou moins une branche pour chaque permutation possible du tableau, en utilisant 2 à 3 comparaisons et au plus 4 assignations pour trier les trois valeurs.

Tri de 4 valeurs

Celui-ci appelle sort3 puis effectue un tri par insertion déroulée avec le dernier élément du tableau:

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]);
            }
        }
    }
}

Cet algorithme effectue 3 à 6 comparaisons et au plus 5 échanges. Il est facile de dérouler un tri d'insertion, mais nous utiliserons un autre algorithme pour le dernier tri ...

Tri de 6 valeurs

Celui-ci utilise une version déroulée de ce que j'ai appelé un tri à double insertion . Le nom n’est pas génial, mais il est assez descriptif, voici comment cela fonctionne:

  • Triez tout sauf les premier et dernier éléments du tableau.
  • Échangez le premier et les éléments du tableau si le premier est supérieur au dernier.
  • Insérez le premier élément dans la séquence triée à partir de l'avant, puis le dernier élément à l'arrière.

Après le swap, le premier élément est toujours plus petit que le dernier, ce qui signifie que lorsqu’ils seront insérés dans la séquence triée, il n’y aura pas plus de N comparaisons pour insérer les deux éléments dans le pire des cas: par exemple, si le le premier élément a été inséré en 3ème position, le dernier élément ne peut pas être inséré plus bas que la 4ème position.

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]);
                }
            }
        }
    }
}

Mes tests sur chaque permutation de 6 valeurs montrent que cet algorithme effectue toujours entre 6 et 13 comparaisons. Je n'ai pas calculé le nombre de swaps effectués, mais je ne m'attends pas à ce qu'il soit supérieur à 11 dans le pire des cas.

J'espère que cela vous aidera, même si cette question ne représente peut-être plus un problème réel :)

EDIT: après l'avoir mis dans le repère fourni, il est clairement plus lent que la plupart des alternatives intéressantes. Cela tend à être un peu plus performant que le type à insertion déroulée, mais c'est à peu près tout. Fondamentalement, ce n'est pas le meilleur type pour les entiers mais peut être intéressant pour les types avec une opération de comparaison coûteuse.

2
Morwenn

Je sais que c'est une vieille question.

Mais je viens d'écrire un type de solution différent que je veux partager.
N'utilisez que MIN imbriqué,

Ce n'est pas rapide car il en utilise 114,
pourrait le réduire à 75 assez simplement comme ceci -> Pastebin

Mais alors ce n'est plus purement min max.

Ce qui pourrait fonctionner est de faire min/max sur plusieurs entiers à la fois avec AVX

référence 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]);
  }
}

MODIFIER:
Solution de classement inspirée de celle de Rex Kerr, beaucoup plus rapide que le bazar précédent

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
PrincePolka

Je crois que votre question comporte deux parties.

  • La première consiste à déterminer l'algorithme optimal. Ceci est fait - du moins dans ce cas-ci - en parcourant tous les ordres possibles (il n'y en a pas autant), ce qui vous permet de calculer les écarts minimum, maximum, moyen et standard exacts des comparaisons et des échanges. Avoir un second ou deux à portée de main aussi.
  • La seconde consiste à optimiser l'algorithme. Beaucoup peut être fait pour convertir des exemples de code de manuels scolaires en algorithmes réels et maigres. Si vous vous rendez compte qu'un algorithme ne peut pas être optimisé autant que nécessaire, essayez un second.

Je ne m'inquiéterais pas trop de vider les canalisations (en supposant que la version actuelle soit x86): la prévision de branche a parcouru un long chemin. Ce qui m'inquiète, c’est de veiller à ce que le code et les données tiennent dans une ligne de cache (peut-être deux pour le code). Une fois sur place, les latences sont rafraîchissantes, ce qui compensera tout décrochage. Cela signifie également que votre boucle interne aura peut-être une dizaine d'instructions, ce qui est exactement ce qu'elle devrait être (il existe deux boucles internes différentes dans mon algorithme de tri, elles ont respectivement 10 instructions/22 octets et une longueur de 9/22). En supposant que le code ne contienne aucun div, vous pouvez être sûr qu'il sera extrêmement rapide.

1
Olof Forshell

J'ai trouvé qu'au moins sur mon système, les fonctions sort6_iterator() et sort6_iterator_local() définies ci-dessous s'exécutaient toutes les deux au moins aussi rapidement et souvent plus rapidement que le détenteur de l'enregistrement actuel ci-dessus:

#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
}

J'ai passé à cette fonction un itérateur de std::vector dans mon code temporel. Je suppose que l’utilisation des itérateurs donne à g ++ certaines assurances quant à ce qui peut et ne peut pas arriver à la mémoire à laquelle l’itérateur fait référence, ce qu’il n’aurait pas autrement. C’est ces assurances qui permettent à g ++ de mieux optimiser le code de tri (qui Je me souviens bien, c’est aussi ne partie de la raison pour laquelle tant d’algorithmes std, tels que std::sort(), ont généralement une si bonne performance). Cependant, pendant le chronométrage, j’ai remarqué que le contexte (c’est-à-dire le code environnant) dans lequel l’appel à la fonction de tri avait été effectué avait un impact significatif sur les performances, ce qui est probablement dû au fait que la fonction était alignée puis optimisée. Par exemple, si le programme était suffisamment simple, il n'y avait généralement pas beaucoup de différence de performances entre passer la fonction de tri d'un pointeur à un itérateur; sinon, utiliser des itérateurs aboutissait généralement à une performance sensiblement meilleure et jamais (d'après mon expérience, au moins) à une performance sensiblement inférieure. Je suppose que cela peut être dû au fait que g ++ peut optimiser globalement un code suffisamment simple.

De plus, sort6_iterator() est quelques-uns fois (toujours selon le contexte dans lequel la fonction est appelée) constamment surperformé par la fonction de tri suivante, qui copie les données dans des variables locales avant de les trier:

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
}

Notez que définir SWAP() comme suit certains fois donne lieu à des performances légèrement meilleures, bien que la plupart du temps, il en résulte des performances légèrement inférieures ou une différence de performance négligeable.

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

Si vous souhaitez simplement un algorithme de tri dont gcc -O3 est toujours capable pour optimiser, puis, en fonction de la manière dont vous transmettez l'entrée, essayez l'un des deux algorithmes suivants:

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
}

Ou (il diffère de ce qui précède par ses 5 premières lignes)

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
}

La raison d'utiliser le mot clé register est qu'il s'agit de l'une des rares fois où vous savez que vous souhaitez que ces valeurs soient stockées dans des registres. Sans register, le compilateur le comprendra la plupart du temps, mais parfois pas. L'utilisation du mot clé register permet de résoudre ce problème. Cependant, n'utilisez normalement pas le mot clé register, car il est plus susceptible de ralentir votre code que de l'accélérer.

Notez également l'utilisation de modèles. Ceci est fait exprès car, même avec le mot clé inline, les fonctions de modèle sont généralement beaucoup plus optimisées par gcc que les fonctions Vanilla C (cela concerne le fait que gcc doive traiter des pointeurs de fonction pour les fonctions Vanilla C mais pas avec des fonctions de modèle).

1
Matthew K.

Essayez de "fusionner la liste triée". :) Utilisez deux tableaux. Le plus rapide pour les petits et les grands.
Si vous concassez, vous ne vérifiez que le lieu d'insertion. D'autres valeurs plus grandes que vous n'avez pas besoin de comparer (cmp = a-b> 0).
Pour 4 chiffres, vous pouvez utiliser le système 4-5 cmp (~ 4.6) ou 3-6 cmp (~ 4.9). Le tri à bulles utilise 6 cmp (6). Beaucoup de cmp pour les gros chiffres code plus lent.
Ce code utilise 5 cmp (pas le type 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);}

Principale MSL 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

code 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
peter

Peut-être que je suis tard dans la soirée, mais au moins ma contribution est une approche nouvelle.

  • Le code vraiment devrait être en ligne
  • même si en ligne, il y a trop de branches
  • la partie analyse est fondamentalement O(N(N-1)), ce qui semble correct pour N = 6
  • le code pourrait être plus efficace si le coût de swap serait plus élevé (tout comme le coût de compare)
  • Je fais confiance aux fonctions statiques étant en ligne.
  • La méthode est liée à la hiérarchisation
    • au lieu des rangs, les relatifs rangs (décalages) sont utilisés.
    • la somme des rangs est égale à zéro pour chaque cycle dans tout groupe de permutation.
    • au lieu de SWAP()ing deux éléments, les cycles sont poursuivis, ne nécessitant qu'un seul temp et un swap (register-> register) (new <- old).

Mise à jour: modifie un peu le code, certaines personnes utilisent des compilateurs C++ pour compiler du code 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);
}
0
wildplasser
//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
}
0
FrantzelasG

Triez 4 articles avec l'utilisation cmp == 0. Le nombre de cmp est ~ 4,34 (~ 4,52 pour le format natif de FF), mais cela prend 3 fois plus de temps que la fusion de listes. Mais il vaut mieux moins d'opérations cmp, si vous avez de gros chiffres ou un gros texte. Edit: bug réparé

Test en ligne 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;
}
0
peter