Suite à l'intérêt que suscite cette question , j'ai pensé qu'il serait intéressant de proposer des réponses un peu plus objectives et quantitatives en proposant un concours.
L'idée est simple: j'ai généré un fichier binaire contenant 50 millions de doubles distribués en gauss (moyenne: 0, stdev 1). Le but est de créer un programme qui les triera en mémoire le plus rapidement possible. Une implémentation de référence très simple en python prend 1m4 à compléter. Jusqu'où pouvons-nous aller?
Les règles sont les suivantes: répondez avec un programme qui ouvre le fichier "gaussian.dat" et trie les nombres en mémoire (il n'est pas nécessaire de les sortir), ainsi que des instructions pour la construction et l'exécution du programme. Le programme doit pouvoir fonctionner sur ma machine Linux Arch (ce qui signifie que vous pouvez utiliser n’importe quel langage ou bibliothèque de programmation facilement installable sur ce système).
Le programme doit être raisonnablement lisible, afin que je puisse m'assurer qu'il peut être lancé en toute sécurité (pas de solution uniquement pour l'assembleur, s'il vous plaît!).
Je vais exécuter les réponses sur ma machine (quad core, 4 gigaoctets de RAM). La solution la plus rapide obtiendra la réponse acceptée et une prime de 100 points :)
Le programme utilisé pour générer les nombres:
#!/usr/bin/env python
import random
from array import array
from sys import argv
count=int(argv[1])
a=array('d',(random.gauss(0,1) for x in xrange(count)))
f=open("gaussian.dat","wb")
a.tofile(f)
L'implémentation de référence simple:
#!/usr/bin/env python
from array import array
from sys import argv
count=int(argv[1])
a=array('d')
a.fromfile(open("gaussian.dat"),count)
print "sorting..."
b=sorted(a)
EDIT: seulement 4 Go de RAM, désolé
EDIT # 2: Notez que le but du concours est de voir si nous pouvons utiliser des informations préalables sur les données . ce n'est pas supposé être un match nul entre différentes implémentations de langage de programmation!
la source
Réponses:
Voici une solution en C ++ qui partitionne d'abord les nombres en compartiments avec le même nombre d'éléments attendu, puis trie chaque compartiment séparément. Il précalcule une table de la fonction de distribution cumulative basée sur certaines formules de Wikipedia, puis interpole les valeurs de cette table pour obtenir une approximation rapide.
Plusieurs étapes s'exécutent dans plusieurs threads pour utiliser les quatre cœurs.
Pour le compiler et l'exécuter, utilisez cette commande:
ÉDITER: Tous les compartiments sont maintenant placés dans le même tableau afin d’éviter de les copier dans le tableau. La taille de la table avec des valeurs précalculées a également été réduite, car les valeurs sont suffisamment précises. Néanmoins, si je change le nombre de compartiments supérieurs à 256, l'exécution du programme prend plus de temps qu'avec ce nombre de compartiments.
EDIT: Le même algorithme, langage de programmation différent. J'ai utilisé C ++ à la place de Java et le temps d'exécution a été réduit de ~ 3.2s à ~ 2.35s sur ma machine. Le nombre optimal de compartiments est toujours autour de 256 (encore une fois, sur mon ordinateur).
À propos, TBB est vraiment génial.
EDIT: Je me suis inspiré de l'excellente solution d'Alexandru et j'ai remplacé la dernière fois std :: sort par une version modifiée de sa sorte radix. J'ai utilisé une méthode différente pour traiter les nombres positifs / négatifs, même s'il faut plus de passages dans le tableau. J'ai également décidé de trier le tableau exactement et de supprimer le tri d'insertion. Je passerai plus tard un peu de temps à tester l’influence de ces modifications sur les performances et éventuellement leur annulation. Cependant, en utilisant le tri de base, le temps a été réduit d’environ 2,35 à environ 1,63.
la source
Sans être malin, pour vous fournir un trieur naïf beaucoup plus rapide, en voici un en C qui devrait être à peu près équivalent à votre trieur Python:
Compilé avec
gcc -O3
, sur ma machine, cela prend plus d’une minute de moins que le Python: environ 11 secondes contre 87 secondes.la source
J'ai partitionné en segments en fonction de l'écart type qui devrait le mieux être divisé en 4ème. Edit: réécrit sur la partition en fonction de la valeur x dans http://en.wikipedia.org/wiki/Error_function#Table_of_values
http://www.wolframalpha.com/input/?i=percentages+by++normal+distribution
J'ai essayé d'utiliser des seaux plus petits, mais cela semblait avoir peu d'effet une fois 2 * sur le nombre de cœurs disponibles. Sans collections parallèles, cela prendrait 37 secondes sur ma boîte et 24 avec les collections parallèles. En cas de partitionnement via une distribution, vous ne pouvez pas simplement utiliser un tableau, il y a donc un surplus de temps système. Je ne suis pas clair sur le moment où une valeur serait boxed / unboxed dans scala.
J'utilise scala 2.9 pour la collection parallèle. Vous pouvez simplement télécharger sa distribution tar.gz.
Pour compiler: scalac SortFile.scala (je viens de le copier directement dans le dossier scala / bin.
Pour exécuter: JAVA_OPTS = "- Xmx4096M" ./scala SortFile (je l'ai exécuté avec 2 Go de RAM et j'ai à peu près le même temps)
Edit: supprimé allocateDirect, plus lent que juste allouer. Suppression de l’amorçage de la taille initiale des tampons de tableau. En fait, il a lu toutes les valeurs 50000000. Réécrit pour éviter, espérons-le, les problèmes de substitution automatique (encore plus lent que naïf c)
la source
Il suffit de mettre cela dans un fichier cs et de le compiler avec csc en théorie: (nécessite mono)
la source
Puisque vous savez quelle est la distribution, vous pouvez utiliser un tri O (N) à indexation directe. (Si vous vous demandez ce que c'est, supposons que vous avez un paquet de 52 cartes et que vous voulez le trier. Ayez juste 52 bacs et jetez chaque carte dans son propre bac.)
Vous avez 5e7 doubles. Allouer un tableau de résultats R de 5e7 doubles. Prenez chaque nombre
x
et obtenezi = phi(x) * 5e7
. Fondamentalement faireR[i] = x
. Avoir un moyen de gérer les collisions, par exemple en déplaçant le nombre avec lequel elle entre en collision (comme dans le codage de hachage simple). Alternativement, vous pouvez rendre R plusieurs fois plus grand, avec une valeur vide unique . À la fin, vous venez de balayer les éléments de R.phi
est juste la fonction de distribution cumulative gaussienne. Il convertit un nombre distribué gaussien entre +/- infini en un nombre distribué uniforme entre 0 et 1. Un moyen simple de le calculer consiste à rechercher dans une table et à effectuer une interpolation.la source
Voici une autre solution séquentielle:
Je doute que cela bat la solution multi-thread, mais les temps sur mon ordinateur portable i7 sont (stdsort est la solution C ++ fournie dans une autre réponse):
Notez que cette solution a une complexité temporelle linéaire (car elle utilise la représentation spéciale des doubles).
EDIT : Correction de l'ordre des éléments à augmenter.
EDIT : amélioration de la vitesse de presque une demi-seconde.
EDIT : amélioration de la vitesse de 0,7 seconde. A rendu l'algorithme plus convivial en cache.
EDIT : amélioration de la vitesse d'une seconde. Comme il n’ya que 50.000.000 d’éléments, je peux partiellement trier la mantisse et utiliser le type insert (qui respecte le cache) pour réparer les éléments déplacés. Cette idée supprime environ deux itérations de la dernière boucle de tri de base.
EDIT : 0,16 secondes en moins. Premièrement, std :: reverse peut être éliminé si l'ordre de tri est inversé.
la source
Prendre la solution de Christian Ammer et la mettre en parallèle avec les blocs de construction filetés d'Intel
Si vous avez accès à la bibliothèque Performance Primitives (IPP) d'Intel, vous pouvez utiliser son type de base. Il suffit de remplacer
avec
et
avec
Sur mon portable dual core, les timings sont
la source
Que diriez-vous d’une implémentation de type quicksort parallèle qui choisisse ses valeurs de pivot en fonction des statistiques de la distribution, assurant ainsi des partitions de taille égale? Le premier pivot serait à la moyenne (zéro dans ce cas), la paire suivante aux 25ème et 75ème centiles (+/- -0,67499 écarts-types), et ainsi de suite, chaque partition divisant par deux le jeu de données restant plus ou moins moins parfaitement.
la source
Très moche (pourquoi utiliser des tableaux quand je peux utiliser des variables finissant par des nombres), mais un code rapide (mon premier essai avec std :: threads), temps entier (temps réel) sur mon système 1,8 s (en comparaison avec std :: sort () 4,8 s), compilez avec g ++ -std = c ++ 0x -O3 -march = natif -pthread Passez simplement des données via stdin (fonctionne uniquement pour 50M).
// Edit modifié pour lire le fichier gaussian.dat.
la source
Une solution C ++ utilisant
std::sort
(éventuellement plus rapide que qsort, en ce qui concerne les performances de qsort vs std :: sort )Je ne peux pas dire
gaussian.dat
avec certitude combien de temps cela prend car je n'ai que 1 Go sur ma machine et avec le code Python donné, je ne pouvais créer qu'un fichier avec seulement 25 millions de doublons (sans erreur de mémoire). Mais je suis très intéressé par la durée d'exécution de l'algorithme std :: sort.la source
sort.h
fichier pour le compiler en C ++. C'était environ deux fois plus lent questd::sort
. Je ne sais pas pourquoi, peut-être à cause des optimisations du compilateur?Voici un mélange de la sorte de radix d'Alexandru avec le pivot intelligent de Zjarek. Compilez-le avec
Vous pouvez changer la taille de la base en définissant STEP (par exemple, ajoutez -DSTEP = 11). J'ai trouvé le meilleur pour mon ordinateur portable est 8 (par défaut).
Par défaut, il divise le problème en 4 morceaux et l'exécute sur plusieurs threads. Vous pouvez changer cela en passant un paramètre de profondeur à la ligne de commande. Donc, si vous avez deux noyaux, lancez-le en tant que
et si vous avez 16 noyaux
La profondeur maximale actuelle est de 6 (64 fils). Si vous mettez trop de niveaux, vous ralentirez simplement le code.
Une chose que j'ai également essayée était le type de base de la bibliothèque Intel Performance Primitives (IPP). La mise en œuvre d'Alexandru va à l'encontre de l'IPP, qui est environ 30% plus lent. Cette variation est également incluse ici (commentée).
EDIT : J'ai mis en œuvre les améliorations du cache d'Alexandru, qui ont permis de réduire d'environ 30% le temps passé sur ma machine.
EDIT : Ceci implémente un tri récursif, donc cela devrait bien fonctionner sur la machine à 16 cœurs d’Alexandru. Il utilise également la dernière amélioration d'Alexandru et supprime l'un des inverses. Pour moi, cela a donné une amélioration de 20%.
EDIT : Correction d'un bug de signe qui causait une inefficacité lorsqu'il y a plus de 2 cœurs.
EDIT : Suppression du lambda, donc il sera compilé avec les anciennes versions de gcc. Il inclut la variation de code IPP commentée. J'ai aussi corrigé la documentation pour courir sur 16 cœurs. Autant que je sache, c'est la mise en œuvre la plus rapide.
EDIT : Correction d'un bug quand STEP n'est pas 8. Augmentation du nombre maximum de threads à 64. Ajout de quelques informations de timing.
la source
step
(11 était optimale sur mon ordinateur portable).int cnt[mask]
devrait êtreint cnt[mask + 1]
. Pour de meilleurs résultats, utilisez une valeur fixeint cnt[1 << 16]
.Je suppose que cela dépend vraiment de ce que vous voulez faire. Si vous voulez trier un groupe de Gaussiens, cela ne vous aidera pas. Mais si vous voulez un groupe de Gaussiens triés, ça ira. Même si cela manque un peu le problème, je pense qu'il sera intéressant de comparer les routines de tri réelles.
Si vous voulez quelque chose pour être rapide, faites-en moins.
Au lieu de générer un groupe d'échantillons aléatoires à partir de la distribution normale, puis de les trier, vous pouvez générer un groupe d'échantillons de la distribution normale dans un ordre trié.
Vous pouvez utiliser la solution ici pour générer n nombres aléatoires uniformes dans un ordre trié. Ensuite, vous pouvez utiliser le cdf inverse (scipy.stats.norm.ppf) de la distribution normale pour transformer les nombres aléatoires uniformes en nombres de la distribution normale via un échantillonnage par transformée inverse .
Si vous voulez avoir les mains plus sales, je suppose que vous pourrez peut-être accélérer les nombreux calculs cdf inverses en utilisant une sorte de méthode itérative et en utilisant le résultat précédent comme première estimation. Puisque les suppositions vont être très proches, une simple itération vous donnera probablement une grande précision.
la source
Essayez cette solution changeante de Guvante avec ce Main (), il commence à trier dès que la lecture 1/4 IO est terminée, le test est plus rapide:
la source
Puisque vous connaissez la distribution, mon idée serait de créer k compartiments, chacun avec le même nombre d’éléments escompté (puisque vous connaissez la distribution, vous pouvez la calculer). Puis, en temps O (n), balayez le tableau et mettez les éléments dans leurs compartiments.
Ensuite, triez simultanément les seaux. Supposons que vous avez k compartiments et n éléments. Un compartiment prendra (n / k) lg (n / k) pour trier. Supposons maintenant que vous ayez p processeurs utilisables. Étant donné que les compartiments peuvent être triés indépendamment, vous devez traiter un multiplicateur de ceil (k / p). Cela donne un temps d'exécution final de n + ceil (k / p) * (n / k) lg (n / k), ce qui devrait être bien plus rapide que n lg n si vous choisissez bien k.
la source
std::sort()
, mais c'est beaucoup plus lent que la solution radixsort d'Alexandru.Une idée d'optimisation de bas niveau consiste à insérer deux doubles dans un registre SSE, afin que chaque thread fonctionne avec deux éléments à la fois. Cela peut être compliqué à faire pour certains algorithmes.
Une autre chose à faire est de trier le tableau en fragments faciles à mettre en cache, puis de fusionner les résultats. Deux niveaux doivent être utilisés: par exemple, 4 Ko pour L1 puis 64 Ko pour L2.
Cela devrait être très convivial pour le cache, car le tri du compartiment ne sortira pas du cache et la fusion finale parcourra la mémoire de manière séquentielle.
De nos jours, le calcul est beaucoup moins cher que les accès en mémoire. Cependant, nous avons un grand nombre d'éléments, il est donc difficile de dire quelle est la taille du tableau lorsque le tri idiote pour le cache est plus lent que pour une version peu complexe sans cache.
Mais je ne fournirai pas d'implémentation de ce qui précède car je le ferais sous Windows (VC ++).
la source
Voici une implémentation du tri du compartiment d'analyse linéaire. Je pense que c'est plus rapide que toutes les implémentations à un seul thread actuelles, à l'exception du tri de base. Le temps d'exécution prévu devrait être linéaire si l'estimation de la cdf est suffisamment précise (j'utilise une interpolation linéaire des valeurs trouvées sur le Web) et je n'ai commis aucune erreur susceptible de provoquer un balayage excessif:
la source
Je ne sais pas, pourquoi je ne peux pas éditer mon précédent post, alors voici la nouvelle version, 0,2 seconde plus rapide (mais environ 1,5 s plus rapide en temps CPU (utilisateur)). Cette solution a 2 programmes, calcule d'abord les quantiles pour une distribution normale pour le tri par compartiment et les stocke dans un tableau, t [double * scale] = index du compartiment, où scale est un nombre arbitraire permettant de doubler le moulage. Ensuite, le programme principal peut utiliser ces données pour placer les doublons dans le compartiment approprié. Cela présente un inconvénient: si les données ne sont pas gaussiennes, elles ne fonctionneront pas correctement (et il n’ya presque aucune chance de fonctionner de manière incorrecte pour une distribution normale), mais la modification pour un cas particulier est facile et rapide ::Trier()).
Compilation: g ++ => http://pastebin.com/WG7pZEzH programme d'aide
g ++ -std = c ++ 0x -O3 -march = natif -pthread => http://pastebin.com/T3yzViZP programme de tri principal
la source
Voici une autre solution séquentielle. Celui-ci utilise le fait que les éléments sont distribués normalement, et je pense que l'idée est généralement applicable pour obtenir un tri proche du temps linéaire.
L'algorithme est comme ça:
phi()
fonction dans l'implémentation)size * phi(x)
Malheureusement, la constante cachée est assez grande et cette solution est deux fois plus lente que l'algorithme de tri de base.
la source
Mon jeu personnel favori utilisant les blocs de construction filetés d'Intel a déjà été publié, mais voici une solution parallèle grossière utilisant JDK 7 et sa nouvelle API fork / join:
Avertissement important : J'ai pris l'adaptation de tri rapide pour fork / rejoindre de: https://github.com/pmbauer/parallel/tree/master/src/main/java/pmbauer/parallel
Pour exécuter cela, vous avez besoin d’une version bêta de JDK 7 (http://jdk7.java.net/download.html).
Sur mon Core i7 2.93Ghz Quad (OS X):
Référence python
Java JDK 7 fork / join
J'ai aussi essayé de faire des essais de lecture en parallèle et de convertir les octets en doubles, mais je n'y voyais aucune différence.
Mise à jour:
Si quelqu'un veut expérimenter le chargement parallèle des données, la version de chargement parallèle est ci-dessous. En théorie, cela pourrait accélérer encore un peu si votre périphérique IO dispose de suffisamment de capacité en parallèle (les disques SSD le sont généralement). La création de doubles à partir d'octets entraîne également des frais généraux, de sorte que cela pourrait aussi aller plus vite en parallèle. Sur mes systèmes (SSD Ubuntu 10.10 / Nehalem Quad / Intel X25M et SSD OS X 10.6 / i7 Quad / Samsung), je ne voyais aucune différence réelle.
Update2:
J'ai exécuté le code sur l'une de nos 12 machines de développement avec une légère modification pour définir un nombre fixe de cœurs. Cela a donné les résultats suivants:
Sur ce système, j'ai également essayé la version Python qui prenait 1m2.994s et la version C ++ de Zjarek qui utilisait 1.925s (pour une raison quelconque, la version C ++ de Zjarek semble fonctionner relativement plus rapidement sur l'ordinateur de static_rtti).
J'ai aussi essayé ce qui se passait si je doublais la taille du fichier à 100 000 000 de fois:
Dans ce cas, la version C ++ de Zjarek a pris 3.968. Python a juste pris trop de temps ici.
150 000 000 doubles:
Dans ce cas, la version C ++ de Zjarek était 6.044s. Je n'ai même pas essayé Python.
La version C ++ est très cohérente avec ses résultats, où Java bascule un peu. Tout d'abord, le problème devient un peu plus efficace lorsque le problème s'aggrave, mais il est à nouveau moins efficace.
la source
Une version utilisant des pthreads traditionnels. Code de fusion copié de la réponse de Guvante. Compiler avec
g++ -O3 -pthread
.Sur mon ordinateur portable, j'obtiens les résultats suivants:
la source
Voici une implémentation C99 séquentielle qui tente de vraiment utiliser la distribution connue. Il effectue essentiellement un seul tour de tri de seau en utilisant les informations de distribution, puis quelques tours de tri rapide sur chaque seau en supposant une distribution uniforme dans les limites du seau et enfin un tri de sélection modifié pour copier les données dans le tampon d'origine. Le tri rapide mémorise les points de division, de sorte que le tri par sélection ne doit fonctionner que sur des petits cunks. Et malgré (parce que?) De toute cette complexité, ce n'est même pas très rapide.
Pour rendre rapide l'évaluation, les valeurs sont échantillonnées en quelques points et, plus tard, seule une interpolation linéaire est utilisée. En fait, peu importe si Φ est évalué exactement, à condition que l'approximation soit strictement monotone.
Les tailles de bacs sont choisies de telle sorte que le risque de débordement du bac est négligeable. Plus précisément, avec les paramètres actuels, la probabilité qu’un jeu de données de 50000000 éléments provoque un débordement de bac est de 3.65e-09. (Ceci peut être calculé en utilisant la fonction de survie de la distribution de Poisson .)
Pour compiler, veuillez utiliser
Comme il y a beaucoup plus de calculs que dans les autres solutions, ces indicateurs de compilation sont nécessaires pour le rendre au moins raisonnablement rapide. Sans
-msse3
les conversions dedouble
pourint
devenir vraiment lent. Si votre architecture ne prend pas en charge SSE3, ces conversions peuvent également être effectuées à l'aide de lalrint()
fonction.Le code est plutôt moche - je ne sais pas si cela répond à l'exigence d'être "raisonnablement lisible" ...
la source
Cela utilise erf () pour placer chaque élément de manière appropriée dans une corbeille, puis trie chaque corbeille. Il maintient le tableau entièrement en place.
Première passe: docensus () compte le nombre d'éléments dans chaque case.
Deuxième passage: partition () permute le tableau, plaçant chaque élément dans son propre bin
Troisième passage: sortbins () effectue un qsort sur chaque bin.
C'est un peu naïf, et appelle la coûteuse fonction erf () deux fois pour chaque valeur. Les premier et troisième passages sont potentiellement parallélisables. La seconde est hautement série et est probablement ralentie par ses modèles d’accès mémoire très aléatoires. Cela peut également valoir la peine de mettre en cache le numéro de bac de chaque double, en fonction du rapport entre la puissance du processeur et la vitesse de la mémoire.
Ce programme vous permet de choisir le nombre de bacs à utiliser. Ajoutez simplement un second numéro à la ligne de commande. Je l'ai compilé avec gcc -O3, mais ma machine est tellement faible que je ne peux pas vous dire de bons résultats.
Edit: Pouf! Mon programme C s'est transformé comme par magie en un programme C ++ utilisant std :: sort!
la source
Jetez un coup d'œil à l'implémentation du tri de base par Michael Herf ( Radix Tricks ). Sur ma machine, le tri était 5 fois plus rapide que l'
std::sort
algorithme de ma première réponse. Le nom de la fonction de tri estRadixSort11
.la source