En répondant à une autre question Stack Overflow ( celle-ci ), je suis tombé sur un sous-problème intéressant. Quel est le moyen le plus rapide pour trier un tableau de 6 entiers?
Comme la question est de très bas niveau:
- nous ne pouvons pas supposer que les bibliothèques sont disponibles (et l'appel lui-même a son coût), seul C
- pour éviter de vider le pipeline d'instructions (qui a un coût très élevé), nous devons probablement minimiser les branches, les sauts et tout autre type de rupture de flux de contrôle (comme ceux cachés derrière les points de séquence dans
&&
ou||
). - l'espace est limité et la minimisation des registres et de l'utilisation de la mémoire est un problème, idéalement en place, le tri est probablement le meilleur.
Vraiment, cette question est une sorte de Golf où le but 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 Zen of Code Optimization de Michael Abrash et ses suites .
Quant à savoir pourquoi il est intéressant, il existe plusieurs couches:
- l'exemple est simple et facile à comprendre et à mesurer, pas beaucoup de compétences C impliquées
- il montre les effets du choix d'un bon algorithme pour le problème, mais aussi les effets du compilateur et du matériel sous-jacent.
Voici ma mise en œuvre de référence (naïve, non optimisée) et mon jeu de 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);
}
Résultats bruts
Comme le nombre de variantes devient important, je les ai toutes rassemblées dans une suite de tests qui peut être trouvée ici . Les tests réels utilisés sont un peu moins naïfs que ceux montré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 sur différentes architectures / compilateurs cibles. (OK les gars, mettez-le dans les réponses, je vais attribuer +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 à l'origine de la solution la plus rapide à l'é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
- Implémentation naïve (tri par insertion): 285,70
- Tri d'insertion (Daniel Stutzbach): 142,12
- Tri par insertion déroulé: 125,47
- Ordre de classement: 102,26
- Ordre de classement avec registres: 58.03
- Réseaux de tri (Daniel Stutzbach): 111,68
- Réseaux de tri (Paul R): 66,36
- Réseaux de tri 12 avec échange rapide: 58,86
- Réseaux de tri 12 réorganisés Échange: 53,74
- Réseaux de tri 12 échange simple réorganisé: 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 des bulles en ligne (Paolo Bonzini): 48,85
- Tri par 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 (tri par insertion): 135.60
- Tri d'insertion (Daniel Stutzbach): 142,11
- Tri par insertion déroulé: 126,75
- Ordre de classement: 46,42
- Ordre de classement avec registres: 43,58
- Réseaux de tri (Daniel Stutzbach): 115,57
- Réseaux de tri (Paul R): 64,44
- Réseaux de tri 12 avec échange rapide: 61,98
- Réseaux de tri 12 réorganisés Échange: 54,67
- Réseaux de tri 12 échange simple réorganisé: 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 des bulles en ligne (Paolo Bonzini): 45,79
- Tri par insertion déroulée (Paolo Bonzini): 80,15
J'ai inclus à la fois les résultats -O1 et -O2 car étonnamment 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 obtenu en évitant la boucle externe. Je l'ai essayé par tri d'insertion déroulée pour vérifier et en effet 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 réel que j'ai utilisé pour tester est ici . Je ne sais pas encore pourquoi elle est presque deux fois plus rapide que l'autre implémentation du réseau de tri. Passage de paramètre? Rapide max?
Réseaux de tri 12 SWAP avec échange rapide
Comme suggéré par Daniel Stutzbach, j'ai combiné son réseau de tri à 12 swaps avec un swap rapide sans branche (le code est ici ). Il est en effet plus rapide, le meilleur jusqu'à présent avec une petite marge (environ 5%) comme on pourrait s'y attendre en utilisant 1 swap de moins.
Il est également intéressant de noter que le swap sans branche semble être beaucoup (4 fois) moins efficace que le simple utilisant if sur l'architecture PPC.
Appeler 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: 10 à 30 fois plus lent ... comme cela est devenu évident avec la nouvelle suite de tests, le principal problème semble être la charge initiale de la bibliothèque après le premier appel, et elle ne se compare pas si mal aux autres version. C'est juste entre 3 et 20 fois plus lent sur mon Linux. Sur certaines architectures utilisées pour les tests par d'autres, cela semble même être plus rapide (je suis vraiment surpris par celle-ci, 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, calculer directement sa position finale. Ceci est efficace car le calcul de l'ordre de classement n'a pas besoin de branchement. L'inconvénient de cette méthode est qu'elle prend trois fois la quantité de mémoire du tableau (une copie du tableau et des variables pour stocker les ordres de classement). Les résultats de performance sont très surprenants (et intéressants). Sur mon architecture de référence avec OS 32 bits et Intel Core2 Quad E8300, le nombre de cycles était légèrement inférieur à 1000 (comme les réseaux de tri avec swap de branchement). Mais lorsqu'elle a été compilée et exécutée sur ma boîte 64 bits (Intel Core2 Duo), elle s'est beaucoup mieux comportée: elle est devenue la plus rapide jusqu'à présent. J'ai finalement découvert la vraie raison. Ma boîte 32 bits utilise gcc 4.4.1 et ma boîte 64 bits gcc 4.4.
mise à jour :
Comme les chiffres publiés ci-dessus le montrent, cet effet a été encore amélioré par les versions ultérieures de gcc et Rank Order est devenu deux fois plus rapide que toute autre alternative.
Réseaux de tri 12 avec échange réorganisé
L'incroyable efficacité de la proposition Rex Kerr avec gcc 4.4.3 m'a fait me demander: comment un programme avec 3 fois plus de mémoire peut-il être plus rapide que les réseaux de tri sans branche? Mon hypothèse était qu'il avait moins de dépendances du type lecture après écriture, permettant une meilleure utilisation du planificateur d'instructions superscalaire du x86. Cela m'a donné une idée: réorganiser les swaps pour minimiser les dépendances de lecture après écriture. Plus simplement: lorsque vous le faites, SWAP(1, 2); SWAP(0, 2);
vous devez attendre la fin du premier échange avant d'effectuer le second car les deux ont accès à une cellule de mémoire commune. Lorsque vous le 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, les réseaux de tri fonctionnent environ 10% plus rapidement.
Tri des réseaux 12 avec échange simple
Un an après la publication originale, 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é à la main en utilisant le code d'assemblage en ligne x86 qui peut encore épargner quelques cycles supplémentaires. Le plus surprenant (il dit des volumes sur la psychologie du programmeur) est qu'il y a un an, aucun des utilisateurs n'avait essayé cette version de swap. Le code que je testais est ici . D'autres ont suggéré d'autres façons d'écrire un échange rapide en C, mais il 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, son simple avantage est d'être court, simple et facile à comprendre ce que nous mesurons), le nombre moyen de cycles du code résultant pour une sorte est inférieur à 40 cycles ( 6 tests sont exécutés). Cela met chaque swap à une moyenne de 4 cycles. J'appelle ça incroyablement rapide. D'autres améliorations possibles?
x-y
etx+y
ne provoquera pas de débordement ou de débordement?__asm__ volatile (".byte 0x0f, 0x31; shlq $32, %%rdx; orq %%rdx, %0" : "=a" (x) : : "rdx");
due au fait que rdtsc place la réponse dans EDX: EAX tandis que GCC l'attend dans un seul registre 64 bits. Vous pouvez voir le bogue en compilant à -O3. Voir également ci-dessous mon commentaire à Paul R sur un SWAP plus rapide.CMP EAX, EBX; SBB EAX, EAX
mettra 0 ou 0xFFFFFFFFEAX
selon qu'ilEAX
est respectivement plus grand ou plus petit queEBX
.SBB
est "soustraire avec emprunter", la contrepartie deADC
("ajouter avec reporter"); le bit d'état auquel vous faites référence est le bit de retenue. Là encore, je me souviens de celaADC
etSBB
avait une latence et un débit terribles sur le Pentium 4 par rapport àADD
etSUB
, et était encore deux fois plus lent sur les processeurs Core. Depuis le 80386, il existe également des instructions deSETcc
stockageCMOVcc
conditionnel et de déplacement conditionnel, mais elles sont également lentes.Réponses:
Pour toute optimisation, il est toujours préférable de tester, tester, tester. J'essaierais au moins les réseaux de tri et le tri par insertion. Si je misais, je mettrais mon argent sur le tri par insertion en fonction de mon expérience passée.
Savez-vous quelque chose sur les données d'entrée? Certains algorithmes fonctionnent mieux avec certains types de données. Par exemple, le tri par insertion fonctionne mieux sur les données triées ou presque triées, ce sera donc le meilleur choix s'il y a une chance supérieure à la moyenne de données presque triées.
L'algorithme que vous avez publié est similaire à un tri par insertion, mais il semble que vous ayez minimisé le nombre de swaps au prix de plus de comparaisons. Les comparaisons sont cependant beaucoup plus chères que les swaps, car les branches peuvent entraîner le blocage du pipeline d'instructions.
Voici une implémentation de tri par insertion:
Voici comment je construisais un réseau de tri. Tout d'abord, utilisez ce 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:
la source
n < SMALL_CONSTANT
.Voici une implémentation utilisant des réseaux de tri :
Vous avez vraiment besoin de branchements
min
et d'max
implémentations très efficaces pour cela, car c'est effectivement ce que ce code se résume à - une séquencemin
et desmax
opérations (13 de chaque, 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 vectorielles min / max) et également aux implémentations GPU (par exemple CUDA - étant sans branche, il n'y a pas de problèmes de divergence de chaîne, etc.).
Voir aussi: Implémentation rapide d'un algorithme pour trier une très petite liste
la source
Sort3
serait plus rapide (sur la plupart des architectures, de toute façon) si vous notiez que(a+b+c)-(min+max)
c'est le numéro central.#define SWAP(x,y) { int dx = d[x], dy = d[y], tmp; tmp = d[x] = dx < dy ? dx : dy; d[y] ^= dx ^ tmp; }
. Ici, je n'utilise pas?: Pour d [y] car cela donne des performances légèrement pires, mais c'est presque dans le bruit.Puisque ce sont des entiers et que les comparaisons sont rapides, pourquoi ne pas calculer directement l'ordre de classement de chacun:
la source
0+1+2+3+4+5=15
Puisqu'un d'entre eux est manquant, 15 moins la somme du reste donne un manquantOn dirait que je suis arrivé à la fête un an en retard, mais c'est parti ...
En regardant l'assembly 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 en mémoire. J'ai ordonné que les charges dans les magasins soient aussi proches que possible de là où 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 à Gunderson, mais gcc est en mesure de mieux ordonner les instructions car elles ne sont pas données comme assemblage explicite.
J'ai utilisé le même ordre de swap que le réseau de swaps réorganisé donné comme le plus performant, bien qu'il puisse y avoir un meilleur ordre. Si je trouve encore du temps, je vais générer et tester un tas de permutations.
J'ai changé le code de test pour prendre en compte plus de 4000 tableaux et afficher le nombre moyen de cycles nécessaires pour trier chacun. Sur un i5-650, j'obtiens ~ 34,1 cycles / tri (en utilisant -O3), par rapport au réseau de tri réorganisé d'origine obtenant ~ 65,3 cycles / tri (en utilisant -O1, bat -O2 et -O3).
J'ai modifié la suite de tests pour signaler également les horloges par tri et exécuter plus de tests (la fonction cmp a également été mise à jour pour gérer le débordement 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 dont je dispose.
la source
-O3
optimisation ne sera pas contre-productive.#define SWAP(x,y) { int oldx = x; x = x < y ? x : y; y ^= oldx ^ x; }
.Je suis tombé sur cette question de Google il y a quelques jours car j'avais également besoin de trier rapidement un tableau de longueur fixe de 6 entiers. Dans mon cas cependant, mes entiers ne sont que de 8 bits (au lieu de 32) et je n'ai pas une exigence stricte d'utiliser uniquement C. Je pensais que je partagerais mes conclusions de toute façon, au cas où elles pourraient être utiles à quelqu'un ...
J'ai implémenté une variante d'un tri réseau dans l'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 de shuffle pour PSHUFB (échange vectorisé), en utilisant uniquement une PADDB (vectorized add) et dans certains cas également une instruction PAND (bitwise AND).
Cette approche a également eu pour effet secondaire de produire une fonction véritablement dépourvue de branches. Il n'y a aucune instruction de saut.
Il apparaît que cette implémentation est environ 38% plus rapide que l'implémentation qui est actuellement marquée comme l'option la plus rapide dans la question ("Tri des réseaux 12 avec échange simple"). J'ai modifié cette implémentation pour utiliser des
char
éléments de tableau lors de mes tests, afin de rendre la comparaison équitable.Je dois 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 vitesse relative par rapport aux alternatives augmente 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...
Vous pouvez le compiler dans un objet exécutable et le lier à votre projet C. Pour obtenir des instructions sur la façon de 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:
la source
pxor / pinsrd xmm4, mem, 0
, utilisez simplementmovd
!Le code de test est assez mauvais; il déborde du tableau initial (les gens ici ne lisent pas les avertissements du compilateur?), le printf imprime les mauvais éléments, il utilise .byte pour rdtsc sans raison valable, il n'y a qu'une seule exécution (!), rien ne vérifie que le les résultats finaux sont en fait corrects (il est donc très facile de «l'optimiser» en quelque chose de subtilement faux), les tests inclus sont très rudimentaires (pas de nombres négatifs?) et rien n'empêche le compilateur de simplement rejeter 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 trucs min / max / SWAP en
et il sort environ 65% plus rapidement pour moi (Debian gcc 4.4.5 avec -O2, amd64, Core i7).
la source
Bien que j'aime vraiment la macro d'échange fournie:
Je vois une amélioration (qu'un bon compilateur pourrait apporter):
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.
la source
d[x]
au lieu dex
(même poury
), etd[y] < d[x]
pour l'inégalité ici (oui, différent du code min / max).N'optimisez jamais les valeurs min / max sans analyse comparative et sans regarder l'assemblage généré par le compilateur. Si je laisse GCC optimiser le min avec des instructions de déplacement conditionnel, j'obtiens une accélération de 33%:
(280 vs 420 cycles dans le code de test). Faire max avec?: Est plus ou moins le même, 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 registres et d'analyse d'alias, déplaçant efficacement d [x] dans les variables locales à l'avance, et ne recopiant qu'en mémoire à la fin. En fait, ils le font encore 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 optimisation forte et que vous essayez de déjouer le compilateur sur min / max. :)Au fait, j'ai essayé Clang et GCC. Ils font la même optimisation, mais en raison de différences de programmation, les deux ont une certaine variation dans les résultats, ne peuvent pas vraiment dire ce qui est plus rapide ou plus lent. GCC est plus rapide sur les réseaux de tri, Clang sur les tris quadratiques.
Par souci d'exhaustivité, le tri à bulles déroulées et les tri par insertion sont également possibles. Voici la sorte de bulle:
et voici le tri par insertion:
Ce type d'insertion est plus rapide que celui de Daniel Stutzbach, et est particulièrement bon sur un GPU ou un ordinateur avec prédication car ITER peut être effectué avec seulement 3 instructions (contre 4 pour SWAP). Par exemple, voici la
t = d[2]; ITER(1); ITER(0);
ligne dans l'assemblage ARM:Pour six éléments, le tri par insertion est compétitif avec le réseau de tri (12 swaps vs 15 itérations équilibre 4 instructions / swap vs 3 instructions / itération); sorte de bulle est bien sûr plus lent. Mais cela ne sera pas vrai lorsque la taille augmentera, car le tri par insertion est O (n ^ 2) tandis que les réseaux de tri sont O (n log n).
la source
J'ai porté la suite de tests sur une machine d'architecture PPC que je ne peux pas identifier (je n'ai pas eu à toucher au code, juste augmenter les itérations du test, utiliser 8 cas de test pour éviter de polluer les résultats avec des mods et remplacer le rdtsc spécifique x86):
Appel direct à la fonction de bibliothèque qsort : 101
Implémentation naïve (tri par insertion) : 299
Tri d'insertion (Daniel Stutzbach) : 108
Tri par insertion déroulé : 51
Réseaux de tri (Daniel Stutzbach) : 26
Réseaux de tri (Paul R) : 85
Réseaux de tri 12 avec échange rapide : 117
Réseaux de tri 12 réorganisés Échange : 116
Ordre de classement : 56
la source
subfc r5,r4,r3; subfe r6,r6,r6; andc r6,r5,r6; add r4,r6,r4; subf r3,r6,r3
. r3 / r4 sont des entrées, r5 / r6 sont des registres à gratter, sur la sortie r3 obtient le minimum et r4 obtient le maximum. Il devrait être décemment programmable à la main. Je l'ai trouvé avec le superoptimiseur GNU, à partir de séquences min et max de 4 instructions et en recherchant manuellement deux qui pourraient être combinées. Pour les entrées signées, vous pouvez bien sûr ajouter 0x80000000 à tous les éléments au début et le soustraire à nouveau à la fin, puis travailler comme s'ils n'étaient pas signés.Un échange XOR peut être utile dans vos fonctions d'échange.
Le if peut causer trop de divergence dans votre code, mais si vous avez la garantie que toutes vos entrées sont uniques, cela pourrait être pratique.
la source
x
ety
pointez au même endroit.J'ai hâte de m'essayer à cela et d'apprendre de ces exemples, mais d'abord quelques timings de mon Powerbook G4 PPC à 1,5 GHz avec 1 Go de RAM DDR. (J'ai emprunté une minuterie similaire à rdtsc pour PPC à http://www.mcs.anl.gov/~kazutomo/rdtsc.html pour les horaires.) J'ai exécuté le programme plusieurs fois et les résultats absolus variaient mais la cohérence le test le plus rapide a été "Insertion Sort (Daniel Stutzbach)", avec "Insertion Sort Unrolled" juste derrière.
Voici le dernier ensemble de fois:
la source
Voici ma contribution à ce fil: un shortsort optimisé à 1, 4 espaces pour un vecteur int à 6 membres (valp) contenant des valeurs uniques.
Sur mon ordinateur portable HP dv7-3010so avec un Athlon M300 à 2 GHz à 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 chronométrage de chaque séquence unique (6! / 720 en tout). Compilé sur Win32 à l'aide d'OpenWatcom 1.8. La boucle est essentiellement un tri par insertion et comporte 16 instructions / 37 octets.
Je n'ai pas d'environnement 64 bits pour compiler.
la source
Si le tri par insertion est raisonnablement compétitif ici, je recommanderais d'essayer un shellsort. Je crains que 6 éléments ne soient probablement pas assez pour être parmi les meilleurs, mais cela pourrait valoir la peine d'être essayé.
Exemple de code, non testé, non débogué, etc. Vous voulez régler la séquence inc = 4 et inc - = 3 pour trouver l'optimum (essayez inc = 2, inc - = 1 par exemple).
Je ne pense pas que cela gagnera, mais si quelqu'un poste une question sur le tri de 10 éléments, qui sait ...
Selon Wikipedia, cela peut même être combiné avec des réseaux de tri: Pratt, V (1979). Shellsort and sorting networks (Thèses exceptionnelles en informatique). Guirlande. ISBN 0-824-04406-1
la source
Je sais que je suis super en retard, mais j'étais intéressé à expérimenter différentes solutions. Tout d'abord, j'ai nettoyé cette pâte, je l'ai compilée et placée dans un référentiel. J'ai gardé des solutions indésirables comme des impasses afin que d'autres ne l'essaient pas. Parmi celles-ci figurait ma première solution, qui tentait de s'assurer que x1> x2 était calculé une fois. Après optimisation, elle n'est pas plus rapide que les autres versions simples.
J'ai ajouté une version en boucle du tri par ordre de classement, car ma propre application de cette étude est de trier 2 à 8 éléments, donc comme il y a un nombre variable d'arguments, une boucle est nécessaire. C'est aussi pourquoi j'ai ignoré les solutions de réseau de tri.
Le code de test n'a pas testé que les doublons étaient traités correctement, donc bien que les solutions existantes soient toutes correctes, j'ai ajouté un cas spécial au code de test pour m'assurer que les doublons ont été traités correctement.
Ensuite, j'ai écrit une sorte d'insertion qui est 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 l'ordre de classement. Je l'ai fait uniquement pour l'expérience et je ne m'attendais pas à ce que cela soit mieux en raison de la ramification en tri par insertion.
Ensuite, j'ai écrit un tri par ordre de classement 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 les indices qu'avec AVX, puis je dois faire un tableau des 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
Le repo peut être trouvé ici: https://github.com/eyepatchParrot/sort6/
la source
vmovmskps
sur des vecteurs entiers (avec un cast pour garder les intrinsèques heureux), en évitant d'avoir à déplacer à droite le résultat bitscan (ffs
).cmpgt
résultat en le soustrayant , au lieu de le masquer avecset1(1)
. par exemple ,index = _mm256_sub_epi32(index, gt)
faitindex -= -1 or 0;
eq = _mm256_insert_epi32(eq, 0, I)
n'est pas un moyen efficace de mettre à zéro un élément s'il se compile tel qu'il est écrit (en particulier pour les éléments en dehors du 4 bas, car ilvpinsrd
n'est disponible qu'avec une destination XMM; les indices supérieurs à 3 doivent être émulés). Au lieu de cela,_mm256_blend_epi32
(vpblendd
) avec un vecteur mis à zéro.vpblendd
est une instruction simple uop qui s'exécute sur n'importe quel port, par rapport à un shuffle qui a besoin du port 5 sur les processeurs Intel. ( agner.org/optimize ).rot
vecteurs avec des shuffles différents à partir de la même source, ou au moins exécuter 2 chaînes dep en parallèle que vous utilisez alternativement, au lieu d'une seule chaîne dep via un shuffle de franchissement de voie (latence de 3 cycles). Cela augmentera l'ILP dans un seul type. 2 chaînes dep limitent le nombre de constantes vectorielles à un nombre raisonnable, juste 2: 1 pour une rotation et une pour 2 étapes de rotation combinées.Cette question devient assez ancienne, mais j'ai dû résoudre le même problème de nos jours: des agorithmes rapides pour trier les petits tableaux. J'ai pensé que ce serait une bonne idée de partager mes connaissances. Alors que j'ai 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 plus petit qu'avec les réseaux de tri, et plus petit qu'avec le tri 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 élevé parfois).
L'algorithme
sort6
utilise l'algorithmesort4
qui utilise l'algorithmesort3
. Voici l'implémentation sous une forme C ++ légère (l'original est lourd de modèles afin qu'il puisse fonctionner avec n'importe quel itérateur à accès aléatoire et toute fonction de comparaison appropriée).Tri de 3 valeurs
L'algorithme suivant est un tri par insertion non déroulé. Lorsque deux swaps (6 affectations) doivent être effectués, il utilise à la place 4 affectations:
Cela semble un peu complexe car le tri a plus ou moins une branche pour chaque permutation possible du tableau, en utilisant 2 à 3 comparaisons et au plus 4 affectations pour trier les trois valeurs.
Tri de 4 valeurs
Celui-ci appelle
sort3
ensuite effectue un tri par insertion non déroulé avec le dernier élément du tableau:Cet algorithme effectue 3 à 6 comparaisons et au plus 5 swaps. Il est facile de dérouler un tri par 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:
Après l'échange, le premier élément est toujours plus petit que le dernier, ce qui signifie que, lors de leur insertion 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 3e position, puis le dernier ne peut pas être inséré plus bas que la 4e position.
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 aide, même si cette question ne représente plus un problème réel :)
EDIT: après l'avoir mis dans le benchmark fourni, il est clairement plus lent que la plupart des alternatives intéressantes. Il a tendance à fonctionner un peu mieux que le type d'insertion déroulée, mais c'est à peu près tout. Fondamentalement, ce n'est pas le meilleur tri pour les entiers mais pourrait être intéressant pour les types avec une opération de comparaison coûteuse.
la source
operator<
pour la comparaison. Outre le nombre objectif de comparaisons et d'échanges, j'ai également correctement chronométré mes algorithmes; cette solution était la plus rapide générique, mais j'ai en effet raté celle de @ RexKerr. Je vais l'essayer :)-O3
. Je suppose que j'adopterai alors une autre stratégie pour ma bibliothèque de tri: fournir trois types d'algorithmes pour avoir soit un faible nombre de comparaisons, un faible nombre de swaps ou potentiellement les meilleures performances. Au moins, ce qui se passera sera transparent pour le lecteur. Merci pour vos idées :)Je pense que votre question comporte deux parties.
Je ne m'inquiéterais pas trop de vider les pipelines (en supposant le x86 actuel): la prédiction de branche a parcouru un long chemin. Ce qui m'inquiéterait, c'est de m'assurer que le code et les données tiennent dans une ligne de cache chacun (peut-être deux pour le code). Une fois qu'il y a fetch, les latences sont rafraîchissantes, ce qui compensera tout blocage. Cela signifie également que votre boucle interne comportera peut-être une dizaine d'instructions, ce qui est exactement là où elle devrait être (il y a deux boucles internes différentes dans mon algorithme de tri, elles sont respectivement de 10 instructions / 22 octets et 9/22 de long). En supposant que le code ne contient pas de divs, vous pouvez être sûr qu'il sera incroyablement rapide.
la source
Je sais que c'est une vieille question.
Mais je viens d'écrire un autre type de solution que je veux partager.
En n'utilisant que du MIN MAX imbriqué,
Ce n'est pas rapide car il utilise 114 de chacun,
pourrait le réduire à 75 tout simplement comme ça -> pastebin
Mais ce n'est plus purement min max.
Ce qui pourrait fonctionner, c'est de faire min / max sur plusieurs entiers à la fois avec AVX
Référence PMINSW
EDIT:
solution de classement basée sur Rex Kerr, beaucoup plus rapide que le gâchis ci-dessus
la source
int16_t
). Mais votre fonction C prétend qu'elle trie un tableau deint
(qui est de 32 bits dans toutes les implémentations C qui prennent en charge cetteasm
syntaxe). L'avez-vous testé avec seulement de petits entiers positifs qui n'ont que 0 dans leurs moitiés hautes? Cela fonctionnera ... Pourint
vous avez besoin de SSE4.1pmin/maxsd
(d = dword). felixcloutier.com/x86/pminsd:pminsq oupminusd
pouruint32_t
.J'ai constaté qu'au moins sur mon système, les fonctions
sort6_iterator()
etsort6_iterator_local()
définies ci-dessous fonctionnaient toutes les deux au moins aussi rapidement, et souvent sensiblement plus vite, que le détenteur du record actuel ci-dessus:J'ai passé cette fonction un
std::vector
itérateur dans mon code temporel.Je soupçonne (à partir de commentaires comme celui-ci et ailleurs) que l'utilisation d'itérateurs donne à g ++ certaines assurances sur 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 et ce sont ces assurances qui permettent à g ++ de mieux optimiser le code de tri (par exemple avec des pointeurs, le compilateur ne peut pas être sûr que tous les pointeurs pointent vers des emplacements mémoire différents). Si je me souviens bien, cela fait également partie de la raison pour laquelle tant d'algorithmes STL, tels que
std::sort()
, ont généralement de si bonnes performances.De plus,
sort6_iterator()
est quelques fois (encore une fois, en fonction du contexte dans lequel la fonction est appelée) surclassent par la fonction de tri suivant, qui copie les données dans les variables locales avant de les trier. 1 Notez que comme il n'y a que 6 variables locales définies, si ces variables locales sont des primitives, elles ne sont probablement jamais réellement stockées dans la RAM et ne sont plutôt stockées que dans les registres du CPU jusqu'à la fin de l'appel de fonction, ce qui aide à effectuer ce tri. fonctionner rapidement. (Cela aide également le compilateur à savoir que des variables locales distinctes ont des emplacements distincts en mémoire).Notez que la définition
SWAP()
suit comme des résultats parfois dans des performances légèrement meilleures , bien que la plupart du temps , il en résulte un peu moins bonnes performances ou une différence négligeable dans la performance.Si vous voulez juste un algorithme de tri qui sur les types de données primitifs, gcc -O3 est toujours bon pour optimiser quel que soit le contexte dans lequel l'appel à la fonction de tri apparaît en 1, puis, selon la façon dont vous passez l'entrée, essayez l'une des deux suivantes algorithmes:
Ou si vous souhaitez passer les variables par référence, utilisez-la (la fonction ci-dessous diffère de la précédente dans ses 5 premières lignes):
La raison de l'utilisation du
register
mot-clé est que c'est l'une des rares fois où vous savez que vous voulez ces valeurs dans les registres. Sansregister
, le compilateur trouvera cela la plupart du temps, mais parfois ce n'est pas le cas. L'utilisation duregister
mot clé permet de résoudre ce problème. Normalement, cependant, n'utilisez pas leregister
mot - clé car il est plus susceptible de ralentir votre code que de l'accélérer.Notez également l'utilisation de modèles. Cela est fait exprès car, même avec le
inline
mot - clé, les fonctions de modèle sont généralement beaucoup plus agressivement optimisées par gcc que les fonctions vanilla C (cela a à voir avec gcc devant traiter des pointeurs de fonction pour les fonctions vanilla C mais pas avec les fonctions de modèle).la source
Essayez de «fusionner la liste triée». :) Utilisez deux tableaux. Le plus rapide pour les petits et grands réseaux.
Si vous concédez, vous vérifiez seulement où insérer. D'autres valeurs plus importantes que vous n'avez pas besoin de comparer (cmp = ab> 0).
Pour 4 numéros, 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 grands nombres de code plus lent.
Ce code utilise 5 cmp (pas le tri 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);}
Principial 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
la source
Triez 4 éléments avec l'utilisation cmp == 0. Le nombre de cmp est ~ 4.34 (FF native a ~ 4.52), mais prend 3 fois plus de temps que la fusion des listes. Mais il vaut mieux moins d'opérations cmp, si vous avez de gros nombres ou du gros texte. Edit: bug réparé
Test en ligne http://mlich.zam.slu.cz/js-sort/x-sort-x2.htm
la source
Je suis peut -être en retard à la fête, mais au moins ma contribution est une nouvelle approche.
swap
était plus élevé (irt le coût decompare
)SWAP()
ingérer deux éléments, les cycles sont poursuivis, ne nécessitant qu'un seul temp, et un swap (registre-> registre) (nouveau <- ancien).Mise à jour: changé un peu le code, certaines personnes utilisent des compilateurs C ++ pour compiler du code C ...
la source
wsort6()
fonction finale a la bonne interface.o1..o5
, il n'est pas nécessaire d'avoir le deuxièmee[6]
tableau temporaire . Et: compiler du code C sur un compilateur C ++ et blâmer le code?#include
. Corrigéla source
Eh bien, si ce n'est que 6 éléments et que vous pouvez tirer parti du parallélisme, vous voulez minimiser la ramification conditionnelle, etc. Pourquoi ne générez-vous pas toutes les combinaisons et testez-vous l'ordre? Je m'aventurerais que dans certaines architectures, cela peut être assez rapide (tant que la mémoire est préallouée)
la source
Voici trois méthodes de tri typiques qui représentent trois classes différentes d'algorithmes de tri:
Mais consultez la discussion de Stefan Nelsson sur l'algorithme de tri le plus rapide? où il discute d'une solution qui descend jusqu'à
O(n log log n)
.. vérifier sa mise en œuvre en CCet algorithme de tri semi-linéaire a été présenté par un article en 1995:
A. Andersson, T. Hagerup, S. Nilsson et R. Raman. Tri en temps linéaire? Dans Actes du 27e Symposium annuel ACM sur la théorie de l'informatique, pages 427-436, 1995.
la source