Maintenant que tout le monde a développé son expertise (souvent étonnante) de codage de bas niveau pour Python, quelle lenteur? (Ou à quelle vitesse votre langue est-elle?) Et à quel point Python est-il vraiment lent (deuxième partie)? Il est temps de relever un défi qui améliorera également votre capacité à améliorer un algorithme.
Le code suivant calcule une liste de longueur 9. Position i
dans la liste compte le nombre de fois où au moins i
des zéros consécutifs ont été trouvés lors du calcul des produits internes entre F
et S
. Pour faire cela exactement, il parcourt toutes les listes F
de longueur possibles n
et les listes S
de longueur n+m-1
.
#!/usr/bin/python
import itertools
import operator
n=8
m=n+1
leadingzerocounts = [0]*m
for S in itertools.product([-1,1], repeat = n+m-1):
for F in itertools.product([-1,1], repeat = n):
i = 0
while (i<m and sum(map(operator.mul, F, S[i:i+n])) == 0):
leadingzerocounts[i] +=1
i+=1
print leadingzerocounts
La sortie est
[4587520, 1254400, 347648, 95488, 27264, 9536, 4512, 2128, 1064]
Si vous augmentez n à 10,12,14,16,18,20 en utilisant ce code, il devient très vite beaucoup trop lent.
Règles
- Le défi consiste à donner la sortie correcte pour le plus grand nombre possible. Seules les valeurs de n sont pertinentes.
- En cas d’égalité, la victoire revient au code le plus rapide de ma machine pour le plus grand n.
- Je me réserve le droit de ne pas tester le code qui prend plus de 10 minutes.
- Vous pouvez modifier l’algorithme de la manière qui vous convient dans la mesure où il donne le bon résultat. En fait, vous devrez changer l’algorithme pour progresser décemment vers la victoire.
- Le gagnant sera récompensé d'une semaine à partir de la date à laquelle la question a été posée.
- La prime sera remise à son échéance, peu après le vainqueur.
Ma machine Les timings seront exécutés sur ma machine. Il s’agit d’une installation ubuntu standard sur un processeur à huit cœurs AMD FX-8350. Cela signifie également que je dois pouvoir exécuter votre code. En conséquence, utilisez uniquement des logiciels libres facilement disponibles et veuillez inclure des instructions complètes sur la compilation et l’exécution de votre code.
Statut .
- C . n = 12 à 49 secondes par @Fors
- Java . n = 16 à 3:07 de @PeterTaylor
- C ++ . n = 16 en 2:21 par @ilmale
- Rpython . n = 22 en 3:11 par @primo
- Java . n = 22 à 6:56 de @PeterTaylor
- Nimrod . n = 24 en 9:28 secondes par @ReimerBehrends
Le gagnant était Reimer Behrends avec une inscription à Nimrod!
À titre de vérification, la sortie pour n = 22 devrait être [12410090985684467712, 2087229562810269696, 351473149499408384, 59178309967151104, 9975110458933248, 1682628717576192, 284866824372224, 48558946385920, 8416739196928, 1518499004416, 301448822784, 71620493312, 22100246528, 8676573184, 3897278464, 1860960256, 911646720, 451520512, 224785920, 112198656, 56062720, 28031360, 14015680]
.
La compétition est terminée mais ... Je vais offrir 200 points pour chaque soumission augmentant n de 2 (moins de 10 minutes sur mon ordinateur) jusqu'à ce que je manque de points. Cette offre est ouverte pour toujours .
la source
Réponses:
Nimrod (N = 22)
Compiler avec
(Nimrod peut être téléchargé ici .)
Cela s'exécute dans le temps imparti pour n = 20 (et pour n = 18 si vous n'utilisez qu'un seul thread, ce qui prend environ 2 minutes dans ce dernier cas).
L'algorithme utilise une recherche récursive, élaguant l'arbre de recherche chaque fois qu'un produit interne différent de zéro est rencontré. Nous avons également réduit l’espace de recherche de moitié en observant que, pour toute paire de vecteurs,
(F, -F)
il suffit de prendre en compte l’un car l’autre produit les mêmes ensembles de produits internes (en inversantS
également).L'implémentation utilise les installations de métaprogrammation de Nimrod pour dérouler / intégrer les premiers niveaux de la recherche récursive. Cela fait gagner un peu de temps lorsque vous utilisez gcc 4.8 et 4.9 comme back-end de Nimrod et une bonne somme pour clang.
On pourrait encore élaguer l’espace de recherche en observant que nous n'avons besoin que de considérer les valeurs de S qui diffèrent par un nombre pair des N premières positions de notre choix de F. Cependant, la complexité ou les besoins en mémoire de celui-ci ne sont pas proportionnels aux grandes valeurs. de N, étant donné que le corps de la boucle est complètement ignoré dans ces cas.
La tabulation où le produit intérieur est égal à zéro semble être plus rapide que d'utiliser une fonctionnalité de comptage de bits dans la boucle. Apparemment, accéder à la table a une très bonne localité.
Il semble que le problème doive être réglé par une programmation dynamique, tenant compte du fonctionnement de la recherche récursive, mais il n’existe aucun moyen apparent de le faire avec une quantité de mémoire raisonnable.
Exemple de sorties:
N = 16:
N = 18:
N = 20:
Pour comparer l'algorithme avec d'autres implémentations, N = 16 prend environ 7,9 secondes sur ma machine avec un seul thread et 2,3 secondes avec quatre cœurs.
N = 22 prend environ 15 minutes sur une machine à 64 cœurs avec 4.4.6 gcc, car le backend de Nimrod et déborde d'entiers 64 bits
leadingZeros[0]
(éventuellement non signés, ils ne l'ont pas examiné).Mise à jour: j'ai trouvé de la place pour quelques améliorations supplémentaires. Premièrement, pour une valeur donnée de
F
, nous pouvons énumérer les 16 premières entrées desS
vecteurs correspondants avec précision, car elles doivent différer exactement à desN/2
endroits. Nous avons donc Précalculer une liste de vecteurs de bits de tailleN
qui ont desN/2
bits ensemble et les utiliser pour tirer la partie initiale deS
partirF
.Deuxièmement, nous pouvons améliorer la recherche récursive en observant que nous connaissons toujours la valeur de
F[N]
(car le MSB a la valeur zéro dans la représentation binaire). Cela nous permet de prédire avec précision dans quelle branche nous recurse à partir du produit intérieur. Cela nous permettrait en fait de transformer toute la recherche en une boucle récursive, mais il arrive en fait que la prédiction de branche soit bousillée un peu, nous avons donc gardé les niveaux supérieurs dans leur forme originale. Nous gagnons encore du temps, principalement en réduisant le nombre de branches que nous effectuons.Pour certains nettoyages, le code utilise maintenant des entiers non signés et les corrige en 64 bits (juste au cas où quelqu'un voudrait l'exécuter sur une architecture 32 bits).
L'accélération globale est comprise entre un facteur x3 et x4. N = 22 a encore besoin de plus de huit cœurs pour fonctionner en moins de 10 minutes, mais sur une machine à 64 cœurs, le temps passe maintenant à environ quatre minutes (avec une
numThreads
montée en puissance correspondante). Je ne pense pas qu'il y ait beaucoup plus de place à l'amélioration sans un algorithme différent, cependant.N = 22:
Mis à jour à nouveau, en utilisant d'autres réductions possibles de l'espace de recherche. Fonctionne dans environ 9:49 minutes pour N = 22 sur ma machine quadcore.
Dernière mise à jour (je pense). De meilleures classes d'équivalence pour les choix de F, réduisant le temps d'exécution de N = 22 à
3:19 minutes57 secondes (edit: j'avais accidentellement exécuté cela avec un seul thread) sur ma machine.Ce changement tient compte du fait qu’une paire de vecteurs produit les mêmes zéros d’avant si l’un peut être transformé en un autre en le faisant pivoter. Malheureusement, une optimisation de bas niveau assez critique nécessite que le bit le plus haut de F dans la représentation de bit soit toujours identique, et tout en utilisant cette équivalence, a considérablement réduit l'espace de recherche et réduit le temps d'exécution d'environ un quart par rapport à l'utilisation d'un espace d'état différent. réduction sur F, les frais généraux liés à l'élimination de l'optimisation de bas niveau l'ont plus que compensée. Cependant, il s'avère que ce problème peut être éliminé en considérant également le fait que F inverses les uns des autres sont également équivalents. Bien que cela ait ajouté un peu à la complexité du calcul des classes d'équivalence, cela m'a également permis de conserver l'optimisation de bas niveau susmentionnée, conduisant à une accélération d'environ x3.
Une mise à jour supplémentaire pour prendre en charge les entiers 128 bits pour les données accumulées. Pour compiler avec des entiers 128 bits, vous aurez besoin
longint.nim
d' ici et avec-d:use128bit
. N = 24 prend toujours plus de 10 minutes, mais j'ai inclus le résultat ci-dessous pour les personnes intéressées.N = 24:
la source
Java (
n=22
?)Je pense que la plupart des réponses qui font mieux que d'
n=16
utiliser une approche similaire à celle-ci, bien qu'elles diffèrent par les symétries qu'elles exploitent et la façon dont elles divisent la tâche entre les threads.Les vecteurs définis dans la question peuvent être remplacés par des chaînes de bits, et le produit interne avec XORing la fenêtre de chevauchement et en vérifiant qu'il existe exactement des
n/2
bits définis (et donc desn/2
bits effacés). Il y a desn! / ((n/2)!)
chaînes den
bits (à coefficient binomial central) avecn/2
jeu de bits (que j'appelle des chaînes équilibrées ), donc pour toute donnée donnée,F
il y a autant de fenêtresS
dont le produit intérieur est nul. De plus, l'action de glisser leS
long de l'un et de vérifier s'il est encore possible de trouver un bit entrant qui donne un produit intérieur nul correspond à la recherche d'une arête dans un graphe dont les nœuds sont les fenêtres et dont les arêtes relient un nœudu
à un nœudv
dont les premiersn-1
bits sont les derniersn-1
des morceaux deu
.Par exemple, avec
n=6
etF=001001
nous obtenons ce graphique:et pour
F=001011
nous obtenons ce graphique:Ensuite , nous devons compter pour chacun
i
de0
lan
nombre de chemins de longueur ,i
il y a, en sommant sur les graphiques pour chaqueF
. Je pense que la plupart d'entre nous utilisons la recherche en profondeur d'abord.Notez que les graphiques sont clairsemés: il est facile de prouver que chaque nœud a un degré d'au plus 1 et un degré au plus un. Cela signifie également que les seules structures possibles sont des chaînes simples et des boucles simples. Cela simplifie un peu le DFS.
J'exploite quelques symétries: les chaînes équilibrées sont fermées sous bit inverse (l'
~
opération dans de nombreuses langues de la famille ALGOL), et sous rotation rotation, de sorte que nous puissions regrouper des valeursF
qui sont liées par ces opérations et ne font que DFS une fois que.Je reçois sur mon Core 2 à 2,5 GHz
Étant donné que l'ordinateur de Lembik possède 8 cœurs et exécute mon programme à thread unique précédent deux fois plus vite que le mien, je suis optimiste qu'il s'exécutera
n=22
dans moins de 8 minutes.la source
C
Il s’agit fondamentalement d’une implémentation légèrement optimisée de l’algorithme dans la question. Il peut gérer
n=12
dans le délai imparti.Test effectué pour
n=12
, y compris la compilation:Commentaire: Je viens d’allumer mon cerveau et d’utiliser une combinatoire simple pour calculer que la première valeur sera toujours
n! / ((n / 2)!)^2 * 2^(n + m - 1)
. Il me semble qu’il faut une solution complètement algébrique à ce problème.la source
Java,
n=16
Pour toute valeur donnée de,
F
il existe des\binom{n}{n/2}
vecteurs qui ont un produit interne nul. Nous pouvons donc construire un graphe dont les sommets sont les vecteurs correspondants et dont les arêtes correspondent au décalage deS
, puis il suffit de compter les chemins de longueur allant jusqu’aun
graphe.Je n’ai pas essayé de microoptimiser cela en remplaçant les conditionnelles par des opérations au niveau des bits, mais chaque incrément double de
n
augmente le temps d'exécution d'environ 16 fois, donc cela ne fera pas assez de différence si je ne suis pas trop près du seuil. Sur ma machine, je ne le suis pas.Je reçois sur mon Core 2 à 2,5 GHz
la source
f
sommets et les sommets de départ, parcourez toutf_xor_g
avec desn/2
bits exactement définis. Pour chacun d'eux, parcourez toutf
et prenezg = f ^ f_xor_g
.RPython, N = 22 ~ 3: 23
Multi-threaded, utilisant une descente récursive sans pile. Le programme accepte deux arguments de ligne de commande: N et le nombre de threads de travail.
Compiler
Créez un clone local du référentiel PyPy en utilisant mercurial, git ou ce que vous préférez. Tapez l'incantation suivante (en supposant que le script ci-dessus est nommé
convolution-high.py
):où
%PYPY_REPO%
représente une variable d'environnement pointant sur le référentiel que vous venez de cloner. La compilation prend environ une minute.Exemples de timings
N = 16, 4 fils:
N = 18, 4 fils:
N = 20, 4 fils:
N = 22, 4 fils:
la source
Python 3.3, N = 20, 3.5min
Clause de non-responsabilité: mon intention n'est PAS de publier cela comme ma propre réponse, car l'algorithme que j'utilise n'est qu'un port sans vergogne de la solution RPython de primo . Mon but ici est seulement de montrer ce que vous pouvez faire en Python si vous combinez la magie de Numpy et Numba modules .
Numba a expliqué en bref:
Mise à jour 1 : Après avoir jeté les chiffres, j'ai remarqué que nous pouvions simplement ignorer complètement certains des chiffres. Alors maintenant, maxf devient (1 << n) // 2 et maxs devient maxf 2 **. Cela accélérera un peu le processus. n = 16 ne prend maintenant que ~ 48 secondes (au lieu de 4,5 minutes). J'ai aussi une autre idée que je vais essayer de voir si je peux la faire aller un peu plus vite.
Mise à jour 2: algorithme modifié (solution de primo). Bien que mon port ne supporte pas encore le multithreading, il est assez trivial d'ajouter. Il est même possible de publier CPython GIL en utilisant Numba et ctypes. Cependant, cette solution fonctionne aussi très vite sur un seul cœur!
Et enfin:
Cela fonctionne sur ma machine en 212688ms ou ~ 3.5min.
la source
C ++ N = 16
Je teste sur un EEEPC avec un atome .. mon temps n'a pas beaucoup de sens. :RÉ
L'atome résolvent n = 14 en 34 secondes. Et n = 16 en 20 minutes. Je veux tester n = 16 sur OP pc. Je suis optimiste.
L'idée est que chaque fois que nous trouvons une solution pour un F donné, nous avons trouvé 2 ^ i solution car nous pouvons changer la partie inférieure de S pour aboutir au même résultat.
compiler:
gcc 26459.cpp -std = c ++ 11 -O3 -march = natif -fstrict-aliasing -ftree-vectorize -Wall -pedantic -o 26459
la source
JAVASCRIPT n: 12
Dans mon ordinateur, cela a pris 231,242 secondes. Dans la démo, j'utilise des webworkers pour éviter de geler le navigateur. Ceci peut être encore amélioré avec les travailleurs parallèles. Je sais que JS n'a aucune chance dans ce défi mais je l'ai fait pour le plaisir!
Cliquez pour lancer la démo en ligne
la source
n=22
[235388928,86292480,19031048,5020640,1657928,783920,545408,481256,463832,460256,459744,459744,459744,459744,459744,459744,459744,459744,459744,459744,459744,459744]
i.imgur.com/FIJa2Ch.pngFortran: n = 12
Je viens de faire une version quick'n'dirty dans Fortran, aucune optimisation sauf OpenMP. Il faut presser juste au-dessous de 10 minutes pour que n = 12 sur la machine OPs, il faut 10:39 sur ma machine, ce qui est un peu plus lent.
Les entiers 64 bits ont en effet un impact négatif sur les performances. Je suppose que je devrais repenser l’ensemble de l’algorithme pour que cela soit beaucoup plus rapide. Je ne sais pas si je vais m'embêter, je pense que je vais plutôt passer un peu de temps à imaginer un bon défi qui soit plus à mon goût. Si quelqu'un d'autre veut le prendre et le suivre, allez-y :)
la source
Lua: n = 16
Clause de non-responsabilité: mon intention n'est PAS de publier ceci comme ma propre réponse, car l'algorithme que j'utilise est volé sans vergogne de la réponse intelligente d' Anna Jokela . qui a été volé sans vergogne de la réponse intelligente de Ilmale .
En outre, ce n'est même pas valide - il y a des inexactitudes causées par des nombres à virgule flottante (il serait préférable que Lua prenne en charge les entiers 64 bits). Cependant, je suis toujours en train de le télécharger, juste pour montrer à quelle vitesse cette solution est rapide. C'est un langage de programmation dynamique, et pourtant je peux calculer n = 16 en un temps raisonnable (1 minute avec un processeur à 800 MHz).
Exécutée avec LuaJIT, l'interpréteur standard est trop lent.
la source
long long
au lieu d'double
un réglage de compilation), pas LuaJIT.