Avertissement
Je sais que les repères artificiels sont mauvais. Ils ne peuvent montrer des résultats que pour une situation étroite très spécifique. Je ne suppose pas qu'une langue est meilleure que l'autre à cause d'un banc stupide. Cependant, je me demande pourquoi les résultats sont si différents. S'il vous plaît voir mes questions en bas.
Description du benchmark mathématique
Le benchmark est de simples calculs mathématiques pour trouver des paires de nombres premiers qui diffèrent de 6 (appelés nombres premiers sexy ) Par exemple, les nombres premiers sexy inférieurs à 100 seraient:(5 11) (7 13) (11 17) (13 19) (17 23) (23 29) (31 37) (37 43) (41 47) (47 53) (53 59) (61 67) (67 73) (73 79) (83 89) (97 103)
Tableau des résultats
Dans le tableau: temps de calcul en secondes En cours d'exécution: tous sauf Factor s'exécutaient dans VirtualBox (invité amd64 instable Debian, hôte Windows 7 x64) CPU: AMD A4-3305M
Sexy primes up to: 10k 20k 30k 100k
Bash 58.00 200.00 [*1] [*1]
C 0.20 0.65 1.42 15.00
Clojure1.4 4.12 8.32 16.00 137.93
Clojure1.4 (optimized) 0.95 1.82 2.30 16.00
Factor n/a n/a 15.00 180.00
Python2.7 1.49 5.20 11.00 119
Ruby1.8 5.10 18.32 40.48 377.00
Ruby1.9.3 1.36 5.73 10.48 106.00
Scala2.9.2 0.93 1.41 2.73 20.84
Scala2.9.2 (optimized) 0.32 0.79 1.46 12.01
[* 1] - J'ai peur d'imaginer combien de temps cela prendra
Listes de codes
C:
int isprime(int x) {
int i;
for (i = 2; i < x; ++i)
if (x%i == 0) return 0;
return 1;
}
void findprimes(int m) {
int i;
for ( i = 11; i < m; ++i)
if (isprime(i) && isprime(i-6))
printf("%d %d\n", i-6, i);
}
main() {
findprimes(10*1000);
}
Rubis:
def is_prime?(n)
(2...n).all?{|m| n%m != 0 }
end
def sexy_primes(x)
(9..x).map do |i|
[i-6, i]
end.select do |j|
j.all?{|j| is_prime? j}
end
end
a = Time.now
p sexy_primes(10*1000)
b = Time.now
puts "#{(b-a)*1000} mils"
Scala:
def isPrime(n: Int) =
(2 until n) forall { n % _ != 0 }
def sexyPrimes(n: Int) =
(11 to n) map { i => List(i-6, i) } filter { _ forall(isPrime(_)) }
val a = System.currentTimeMillis()
println(sexyPrimes(100*1000))
val b = System.currentTimeMillis()
println((b-a).toString + " mils")
Scala optimisé isPrime
(la même idée que dans l'optimisation Clojure):
import scala.annotation.tailrec
@tailrec // Not required, but will warn if optimization doesn't work
def isPrime(n: Int, i: Int = 2): Boolean =
if (i == n) true
else if (n % i != 0) isPrime(n, i + 1)
else false
Clojure:
(defn is-prime? [n]
(every? #(> (mod n %) 0)
(range 2 n)))
(defn sexy-primes [m]
(for [x (range 11 (inc m))
:let [z (list (- x 6) x)]
:when (every? #(is-prime? %) z)]
z))
(let [a (System/currentTimeMillis)]
(println (sexy-primes (* 10 1000)))
(let [b (System/currentTimeMillis)]
(println (- b a) "mils")))
Clojure optimisé is-prime?
:
(defn ^:static is-prime? [^long n]
(loop [i (long 2)]
(if (= (rem n i) 0)
false
(if (>= (inc i) n) true (recur (inc i))))))
Python
import time as time_
def is_prime(n):
return all((n%j > 0) for j in xrange(2, n))
def primes_below(x):
return [[j-6, j] for j in xrange(9, x+1) if is_prime(j) and is_prime(j-6)]
a = int(round(time_.time() * 1000))
print(primes_below(10*1000))
b = int(round(time_.time() * 1000))
print(str((b-a)) + " mils")
Facteur
MEMO:: prime? ( n -- ? )
n 1 - 2 [a,b] [ n swap mod 0 > ] all? ;
MEMO: sexyprimes ( n n -- r r )
[a,b] [ prime? ] filter [ 6 + ] map [ prime? ] filter dup [ 6 - ] map ;
5 10 1000 * sexyprimes . .
Bash (zsh):
#!/usr/bin/zsh
function prime {
for (( i = 2; i < $1; i++ )); do
if [[ $[$1%i] == 0 ]]; then
echo 1
exit
fi
done
echo 0
}
function sexy-primes {
for (( i = 9; i <= $1; i++ )); do
j=$[i-6]
if [[ $(prime $i) == 0 && $(prime $j) == 0 ]]; then
echo $j $i
fi
done
}
sexy-primes 10000
Des questions
- Pourquoi Scala est si rapide? Est-ce à cause du typage statique ? Ou il utilise simplement JVM de manière très efficace?
Pourquoi une telle différence entre Ruby et Python? Je pensais que ces deux n'étaient pas totalement différents. Peut-être que mon code est erroné. Veuillez m'éclairer! Merci.UPD Oui, c'était une erreur dans mon code. Python et Ruby 1.9 sont assez égaux.- Saut de productivité vraiment impressionnant entre les versions de Ruby.
- Puis-je optimiser le code Clojure en ajoutant des déclarations de type? Cela aidera-t-il?
sqrt(n)
mais cela peut prendre un certain temps à calculer. De plus, votre code C imprime les nombres premiers au fur et à mesure qu'il les trouve, tandis que vos autres langages les calculent en listes puis les imprime. Bien que C soit sans surprise le plus rapide, vous pourrez peut-être l'obtenir plus rapidement.C: 2.723s
Go: 2.743s
.sqrt
pour cette vérification. Vous pouvez calculer le carré dei
comme dansfor (i = 2; i * i <= x; ++i) ...
isPrime
avec@tailrec
, pour vous assurer que vous utilisez la récursivité de queue. Il est facile de faire quelque chose par erreur qui empêche la récursivité de la queue, et cette annotation devrait vous avertir si cela se produit.Réponses:
Réponses approximatives:
(2...n).all?
dans la fonctionis-prime?
est susceptible d'être assez bien optimisée dans Ruby (EDIT: cela semble être le cas, voir la réponse de Julian pour plus de détails ...)L'optimisation la plus importante dans le code Clojure serait d'utiliser des mathématiques primitives typées à l'intérieur
is-prime?
, quelque chose comme:Avec cette amélioration, Clojure termine 10 km en 0,635 secondes (c'est-à-dire le deuxième plus rapide de votre liste, battant Scala)
PS notez que vous avez l'impression de code dans votre benchmark dans certains cas - ce n'est pas une bonne idée car cela déformera les résultats, surtout si l'utilisation d'une fonction comme
print
pour la première fois provoque l'initialisation des sous-systèmes IO ou quelque chose du genre!la source
is-prime?
montre une amélioration 2x. ;)(zero? (mod n i))
devrait être plus rapide que(= (mod n i) 0)
Voici une version rapide de Clojure, utilisant les mêmes algorithmes de base:
Il fonctionne environ 20 fois plus vite que votre original sur ma machine. Et voici une version qui exploite la nouvelle bibliothèque de réducteurs en 1.5 (nécessite Java 7 ou JSR 166):
Cela fonctionne environ 40 fois plus vite que votre original. Sur ma machine, c'est 100k en 1,5 seconde.
la source
unchecked-remainder-int
ou simplementrem
au lieu demod
avec des résultats de frappe statiques pour multiplier par quatre les performances. Agréable!Je répondrai juste n ° 2, car c'est le seul que j'ai quelque chose d'intelligent à dire à distance, mais pour votre code Python, vous créez une liste intermédiaire dans
is_prime
, alors que vous utilisez.map
dans votreall
dans Ruby qui est juste itération.Si vous changez votre
is_prime
en:ils sont à égalité.
Je pourrais optimiser davantage Python, mais mon Ruby n'est pas assez bon pour savoir quand j'ai donné plus d'avantage (par exemple, utiliser
xrange
fait gagner Python sur ma machine, mais je ne me souviens pas si la gamme Ruby que vous avez utilisée crée une plage entière en mémoire ou non).EDIT: Sans être trop ridicule, faire ressembler le code Python à:
ce qui ne change pas beaucoup plus, le met à 1,5 s pour moi, et, en étant très ridicule, l'exécuter avec PyPy le met à 0,3s pour 10K et 21s pour 100K.
la source
False
(bonne prise).xrange
! J'ai corrigé et maintenant Python et Ruby affichent des résultats égaux.lru_cache
implémentation aléatoire pour 2.7 trouvée sur AS, 100K s'exécute en 2.3s.Vous pouvez rendre la Scala beaucoup plus rapide en modifiant votre
isPrime
méthode pourPas aussi concis mais le programme fonctionne dans 40% du temps!
Nous supprimons les objets superflus
Range
et anonymesFunction
, le compilateur Scala reconnaît la récursion de queue et la transforme en une boucle while, que la JVM peut transformer en code machine plus ou moins optimal, donc il ne devrait pas être trop éloigné du C version.Voir aussi: Comment optimiser les for-compréhensions et les boucles dans Scala?
la source
i == n || n % i != 0 && isPrime(n, i + 1)
, qui est plus court, bien qu'un peu plus difficile à lire@tailrec
annotation pour vous assurer qu'elle effectuera cette optimisation.Voici ma version scala à la fois parallèle et non parallèle, juste pour le plaisir: (Dans mon calcul dual core, la version parallèle prend 335 ms tandis que la version non parallèle prend 655 ms)
EDIT: Selon la suggestion d' Emil H , j'ai changé mon code pour éviter les effets du préchauffage IO et jvm:
Le résultat s'affiche dans mon calcul:
la source
isSexyPrime
pourrait être (plus) optimisé lorsqu'il est appelé depuisfindPrimesPar
et pas tellement lorsqu'il est appelé depuisfindPrimes
Peu importe les repères; le problème m'a intéressé et j'ai fait quelques ajustements rapides. Cela utilise le
lru_cache
décorateur, qui mémorise une fonction; Ainsi, lorsque nous appelons,is_prime(i-6)
nous obtenons ce chèque de premier ordre gratuitement. Ce changement réduit le travail à peu près de moitié. En outre, nous pouvons faire passer lesrange()
appels uniquement par les nombres impairs, ce qui réduit le travail à peu près de moitié.http://en.wikipedia.org/wiki/Memoization
http://docs.python.org/dev/library/functools.html
Cela nécessite Python 3.2 ou plus récent pour obtenir
lru_cache
, mais pourrait fonctionner avec un ancien Python si vous installez une recette Python qui fournitlru_cache
. Si vous utilisez Python 2.x, vous devriez vraiment utiliser à laxrange()
place derange()
.http://code.activestate.com/recipes/577479-simple-caching-decorator/
Ce qui précède n'a pris que très peu de temps à modifier. J'ai décidé d'aller plus loin et de faire en sorte que le test des nombres premiers n'essaie que les diviseurs premiers, et seulement jusqu'à la racine carrée du nombre testé. La façon dont je l'ai fait ne fonctionne que si vous vérifiez les nombres dans l'ordre, afin qu'il puisse accumuler tous les nombres premiers au fur et à mesure; mais ce problème vérifiait déjà les nombres dans l'ordre, donc c'était bien.
Sur mon ordinateur portable (rien de spécial; le processeur est un AMD Turion II "K625" 1,5 GHz), cette version a produit une réponse pour 100K en moins de 8 secondes.
Le code ci-dessus est assez facile à écrire en Python, Ruby, etc. mais serait plus pénible en C.
Vous ne pouvez pas comparer les chiffres de cette version avec les chiffres des autres versions sans réécrire les autres pour utiliser des astuces similaires. Je n'essaye pas de prouver quoi que ce soit ici; Je pensais juste que le problème était amusant et je voulais voir quel genre d'améliorations de performances faciles je pourrais glaner.
la source
lru_cache
est vraiment chouette. Pour certaines classes de problèmes, comme la génération de nombres de Fibonacci successifs, cela peut donner une énorme accélération simplement en ajoutant ce décorateur de ligne à la fonction! Voici un lien vers une conférence de Raymond Hettinger qui couvrelru_cache
environ 26 minutes. Blip.tv/pycon-us-videos-2009-2010-2011/…lru_cache
évite de répéter un calcul qui a déjà été fait récemment, et c'est tout; Je ne vois pas comment cela "utilise en fait un autre algorithme". Et Python souffre d'être lent, mais profite d'avoir des trucs sympas commelru_cache
; Je ne vois rien de mal à utiliser les parties bénéfiques d'une langue. Et j'ai dit qu'il ne fallait pas comparer le temps d'exécution de ma réponse avec les autres langages sans apporter des changements similaires aux autres. Donc, je ne comprends pas ce que tu veux dire.0.03
secondes (30
ms) .N'oubliez pas Fortran! (Surtout en plaisantant, mais je m'attendrais à des performances similaires à C). Les instructions avec des points d'exclamation sont facultatives, mais de bon style. (
!
est un caractère de commentaire dans fortran 90)la source
Je n'ai pas pu résister à quelques-unes des optimisations les plus évidentes pour la version C qui faisaient que le test de 100k prenait maintenant 0,3 s sur ma machine (5 fois plus rapide que la version C de la question, toutes deux compilées avec MSVC 2010 / Ox) .
Voici l'implémentation identique en Java:
Avec Java 1.7.0_04, cela fonctionne presque exactement aussi vite que la version C. La VM cliente ou serveur ne montre pas beaucoup de différence, sauf que la formation JIT semble aider un peu la VM serveur (~ 3%) alors qu'elle n'a presque aucun effet avec la VM cliente. La sortie en Java semble être plus lente qu'en C. Si la sortie est remplacée par un compteur statique dans les deux versions, la version Java s'exécute un peu plus vite que la version C.
Voici mes temps pour la course de 100 km:
et la course 1M (16386 résultats):
Bien que cela ne réponde pas vraiment à vos questions, cela montre que de petites modifications peuvent avoir un impact notable sur les performances. Donc, pour pouvoir vraiment comparer les langues, vous devez essayer d'éviter autant que possible toutes les différences algorithmiques.
Cela donne également une idée des raisons pour lesquelles Scala semble plutôt rapide. Il fonctionne sur la machine virtuelle Java et bénéficie ainsi de ses performances impressionnantes.
la source
Dans Scala, essayez d'utiliser Tuple2 au lieu de List, cela devrait aller plus vite. Supprimez simplement le mot 'List' puisque (x, y) est un Tuple2.
Tuple2 est spécialisé pour Int, Long et Double, ce qui signifie qu'il n'aura pas à boxer / déballer ces types de données bruts. Source Tuple2 . La liste n'est pas spécialisée. Liste source .
la source
forall
. J'ai aussi pensé que ce n'était peut-être pas le code le plus efficace (davantage parce qu'une grande collection stricte est créée pour les grandsn
au lieu de simplement utiliser une vue), mais il est certainement court + élégant, et j'ai été surpris de voir à quel point il fonctionnait bien malgré l'utilisation d'un beaucoup de style fonctionnel.def sexyPrimes(n: Int) = (11 to n).map(i => (i-6, i)).filter({ case (i, j) => isPrime(i) && isPrime(j) })
c'est environ 60% plus rapide ici, donc devrait battre le code C :)collect
nettement plus lent. Le plus rapide est si vous effectuez d'abord le filtre, puis la carte.withFilter
est légèrement plus rapide car il ne crée pas réellement de collections intermédiaires.(11 to n) withFilter (i => isPrime(i - 6) && isPrime(i)) map (i => (i - 6, i))
Voici le code de la version Go (golang.org):
Il a fonctionné aussi vite que la version C.
Utilisation d'un Asus u81a Intel Core 2 Duo T6500 2,1 GHz, 2 Mo de cache L2, bus frontal à 800 MHz. 4 Go de RAM
La version 100k:
C: 2.723s
Go: 2.743s
Avec 1000000 (1M au lieu de 100K):
C: 3m35.458s
Go: 3m36.259s
Mais je pense qu'il serait juste d'utiliser les capacités multithreading intégrées de Go et de comparer cette version avec la version C standard (sans multithreading), simplement parce qu'il est presque trop facile de faire du multithreading avec Go.
Mise à jour: j'ai fait une version parallèle en utilisant Goroutines dans Go:
La version parallélisée utilisée en moyenne 2,743 secondes, exactement le même temps que la version régulière utilisée.La version parallélisée s'est terminée en 1,706 secondes. Il a utilisé moins de 1,5 Mo de RAM.
Une chose étrange: mon kubuntu 64 bits à double cœur n'a jamais atteint un sommet dans les deux cœurs. Il semblait que Go n'utilisait qu'un seul noyau.Fixé avec un appel àruntime.GOMAXPROCS(4)
Mise à jour: j'ai exécuté la version paralellisée jusqu'à 1M de numéros.
L'un de mes cœurs de processeur était à 100% tout le temps, tandis que l'autre n'était pas du tout utilisé (étrange). Cela a pris une minute entière de plus que le C et les versions régulières de Go. :(Avec 1000000 (1M au lieu de 100K):
C: 3m35.458s
Go: 3m36.259s
Go using goroutines:
3m27.137s2m16.125s
La version 100k:
C: 2.723s
Go: 2.743s
Go using goroutines: 1.706s
la source
-O3
ou mieux.Juste pour le plaisir, voici une version parallèle de Ruby.
Sur mon MacBook Air Core i5 à 1,8 GHz, les performances sont les suivantes:
Il semble que le JIT de la JVM donne à Ruby une belle amélioration des performances dans le cas par défaut, tandis que le véritable multithreading aide JRuby à être 50% plus rapide dans le cas fileté. Ce qui est plus intéressant, c'est que JRuby 1.7 améliore le score de JRuby 1.6 de 17%!
la source
Sur la base de la réponse de x4u , j'ai écrit une version scala en utilisant la récursivité, et je l'ai améliorée en allant uniquement dans sqrt au lieu de x / 2 pour la fonction de vérification principale. J'obtiens ~ 250ms pour 100k et ~ 600ms pour 1M. Je suis allé de l'avant et je suis allé à 10M en ~ 6s.
Je suis aussi retourné et j'ai écrit une version CoffeeScript (V8 JavaScript), qui obtient ~ 15ms pour 100k, 250ms pour 1M et 6s pour 10M, en utilisant un compteur (en ignorant les E / S). Si j'active la sortie, cela prend environ 150 ms pour 100k, 1s pour 1M et 12s pour 10M. Impossible d'utiliser la récursivité de la queue ici, malheureusement, j'ai donc dû la reconvertir en boucles.
la source
La réponse à votre question n ° 1 est que oui, la JVM est incroyablement rapide et oui, la saisie statique aide.
La JVM devrait être plus rapide que C à long terme, peut-être même plus rapide que le langage d'assemblage "normal" - Bien sûr, vous pouvez toujours optimiser l'assemblage pour battre n'importe quoi en effectuant un profil d'exécution manuel et en créant une version distincte pour chaque processeur, il vous suffit doivent être incroyablement bons et bien informés.
Les raisons de la vitesse de Java sont:
La JVM peut analyser votre code pendant son exécution et l'optimiser manuellement - par exemple, si vous aviez une méthode qui pouvait être analysée statiquement au moment de la compilation pour être une vraie fonction et que la JVM a remarqué que vous l'appeliez souvent avec le même paramètres, il POURRAIT en fait éliminer complètement l'appel et injecter simplement les résultats du dernier appel (je ne sais pas si Java fait exactement cela, mais il fait beaucoup de choses comme ça).
En raison du typage statique, la JVM peut en savoir beaucoup sur votre code au moment de la compilation, ce qui lui permet de pré-optimiser un peu de choses. Il permet également au compilateur d'optimiser chaque classe individuellement sans savoir comment une autre classe envisage de l'utiliser. De plus, Java n'a pas de pointeurs arbitraires vers l'emplacement de la mémoire, il SAIT quelles valeurs en mémoire peuvent et ne peuvent pas être modifiées et peut optimiser en conséquence.
L'allocation de tas est BEAUCOUP plus efficace que C, l'allocation de tas de Java ressemble plus à l'allocation de pile de C en vitesse - mais plus polyvalente. Beaucoup de temps a été consacré aux différentes algroithmes utilisées ici, c'est un art - par exemple, tous les objets à courte durée de vie (comme les variables de pile de C) sont alloués à un emplacement libre "connu" (pas de recherche de place libre avec suffisamment d'espace) et sont tous libérés ensemble en une seule étape (comme une pile pop).
La JVM peut connaître les particularités de l'architecture de votre CPU et générer du code machine spécifiquement pour un CPU donné.
La JVM peut accélérer votre code longtemps après l'avoir expédié. Tout comme déplacer un programme vers un nouveau processeur peut l'accélérer, le déplacer vers une nouvelle version de la JVM peut également vous donner des performances de vitesse énormes adaptées à des processeurs qui n'existaient même pas lorsque vous avez initialement compilé votre code, quelque chose que c ne peut physiquement pas faire sans un recomiple.
À propos, la plupart des mauvaises représentants de la vitesse java proviennent du long temps de démarrage pour charger la JVM (un jour, quelqu'un construira la JVM dans le système d'exploitation et cela disparaîtra!) Et du fait que de nombreux développeurs sont vraiment mauvais en écriture Code GUI (en particulier threadé) qui faisait que les interfaces graphiques Java devenaient souvent insensibles et glitchy. Les langages simples à utiliser comme Java et VB ont leurs défauts amplifiés par le fait que les capacités du programmeur moyen ont tendance à être inférieures à celles des langages plus compliqués.
la source