Comparaison de vitesse avec Project Euler: C vs Python vs Erlang vs Haskell


673

J'ai pris le problème # 12 de Project Euler comme un exercice de programmation et pour comparer mes implémentations (sûrement pas optimales) en C, Python, Erlang et Haskell. Afin d'obtenir des temps d'exécution plus élevés, je recherche le premier numéro de triangle avec plus de 1000 diviseurs au lieu de 500 comme indiqué dans le problème d'origine.

Le résultat est le suivant:

C:

lorenzo@enzo:~/erlang$ gcc -lm -o euler12.bin euler12.c
lorenzo@enzo:~/erlang$ time ./euler12.bin
842161320

real    0m11.074s
user    0m11.070s
sys 0m0.000s

Python:

lorenzo@enzo:~/erlang$ time ./euler12.py 
842161320

real    1m16.632s
user    1m16.370s
sys 0m0.250s

Python avec PyPy:

lorenzo@enzo:~/Downloads/pypy-c-jit-43780-b590cf6de419-linux64/bin$ time ./pypy /home/lorenzo/erlang/euler12.py 
842161320

real    0m13.082s
user    0m13.050s
sys 0m0.020s

Erlang:

lorenzo@enzo:~/erlang$ erlc euler12.erl 
lorenzo@enzo:~/erlang$ time erl -s euler12 solve
Erlang R13B03 (erts-5.7.4) [source] [64-bit] [smp:4:4] [rq:4] [async-threads:0] [hipe] [kernel-poll:false]

Eshell V5.7.4  (abort with ^G)
1> 842161320

real    0m48.259s
user    0m48.070s
sys 0m0.020s

Haskell:

lorenzo@enzo:~/erlang$ ghc euler12.hs -o euler12.hsx
[1 of 1] Compiling Main             ( euler12.hs, euler12.o )
Linking euler12.hsx ...
lorenzo@enzo:~/erlang$ time ./euler12.hsx 
842161320

real    2m37.326s
user    2m37.240s
sys 0m0.080s

Sommaire:

  • C: 100%
  • Python: 692% (118% avec PyPy)
  • Erlang: 436% (135% grâce à RichardC)
  • Haskell: 1421%

Je suppose que C a un gros avantage car il utilise long pour les calculs et non des entiers de longueur arbitraire comme les trois autres. De plus, il n'a pas besoin de charger un runtime en premier (les autres?).

Question 1: Erlang, Python et Haskell perdent-ils de la vitesse en raison de l'utilisation d'entiers de longueur arbitraire ou pas tant que les valeurs sont inférieures à MAXINT?

Question 2: Pourquoi Haskell est-il si lent? Existe-t-il un indicateur de compilation qui désactive les freins ou est-ce mon implémentation? (Ce dernier est tout à fait probable car Haskell est un livre avec sept sceaux pour moi.)

Question 3: pouvez-vous me donner quelques conseils pour optimiser ces implémentations sans changer la façon dont je détermine les facteurs? Optimisation de toute façon: plus agréable, plus rapide, plus "natif" de la langue.

ÉDITER:

Question 4: Mes implémentations fonctionnelles permettent-elles le LCO (optimisation du dernier appel, également appelé élimination de la récursivité de la queue) et évitent ainsi d'ajouter des trames inutiles à la pile d'appels?

J'ai vraiment essayé d'implémenter le même algorithme aussi similaire que possible dans les quatre langues, même si je dois admettre que mes connaissances en Haskell et Erlang sont très limitées.


Codes sources utilisés:

#include <stdio.h>
#include <math.h>

int factorCount (long n)
{
    double square = sqrt (n);
    int isquare = (int) square;
    int count = isquare == square ? -1 : 0;
    long candidate;
    for (candidate = 1; candidate <= isquare; candidate ++)
        if (0 == n % candidate) count += 2;
    return count;
}

int main ()
{
    long triangle = 1;
    int index = 1;
    while (factorCount (triangle) < 1001)
    {
        index ++;
        triangle += index;
    }
    printf ("%ld\n", triangle);
}

#! /usr/bin/env python3.2

import math

def factorCount (n):
    square = math.sqrt (n)
    isquare = int (square)
    count = -1 if isquare == square else 0
    for candidate in range (1, isquare + 1):
        if not n % candidate: count += 2
    return count

triangle = 1
index = 1
while factorCount (triangle) < 1001:
    index += 1
    triangle += index

print (triangle)

-module (euler12).
-compile (export_all).

factorCount (Number) -> factorCount (Number, math:sqrt (Number), 1, 0).

factorCount (_, Sqrt, Candidate, Count) when Candidate > Sqrt -> Count;

factorCount (_, Sqrt, Candidate, Count) when Candidate == Sqrt -> Count + 1;

factorCount (Number, Sqrt, Candidate, Count) ->
    case Number rem Candidate of
        0 -> factorCount (Number, Sqrt, Candidate + 1, Count + 2);
        _ -> factorCount (Number, Sqrt, Candidate + 1, Count)
    end.

nextTriangle (Index, Triangle) ->
    Count = factorCount (Triangle),
    if
        Count > 1000 -> Triangle;
        true -> nextTriangle (Index + 1, Triangle + Index + 1)  
    end.

solve () ->
    io:format ("~p~n", [nextTriangle (1, 1) ] ),
    halt (0).

factorCount number = factorCount' number isquare 1 0 - (fromEnum $ square == fromIntegral isquare)
    where square = sqrt $ fromIntegral number
          isquare = floor square

factorCount' number sqrt candidate count
    | fromIntegral candidate > sqrt = count
    | number `mod` candidate == 0 = factorCount' number sqrt (candidate + 1) (count + 2)
    | otherwise = factorCount' number sqrt (candidate + 1) count

nextTriangle index triangle
    | factorCount triangle > 1000 = triangle
    | otherwise = nextTriangle (index + 1) (triangle + index + 1)

main = print $ nextTriangle 1 1

55
@Jochen (et Seth) Pas vraiment que C est rapide ou génial, mais il est perçu comme facile à écrire du code performant (ce n'est peut-être pas vrai, mais la plupart des programmes semblent pouvoir le faire, donc assez vrai). Comme je l'explore dans ma réponse, et que j'ai trouvé vrai au fil du temps, la compétence du programmeur et la connaissance des optimisations courantes pour le langage choisi sont d'une grande importance (en particulier pour Haskell).
Thomas M. DuBuisson

52
Il suffit de vérifier avec Mathematica - il faut 0.25sec (avec C il faut 6sec ici), et le code est juste: Euler12[x_Integer] := Module[{s = 1}, For[i = 2, DivisorSigma[0, s] < x, i++, s += i]; s]. Hourra!
tsvikas

35
Y a-t-il quelqu'un d'autre qui se souvient de ces guerres entre C et l'assemblée? "Bien sûr! Vous pouvez écrire votre code 10 fois plus vite en C, mais votre code C peut-il fonctionner aussi vite? ..." Je suis sûr que les mêmes batailles ont été menées entre le code machine et l'assemblage.
JS.

39
@JS: Probablement pas, car l'assemblage est simplement un ensemble de mnémoniques que vous tapez au lieu du code machine binaire brut - normalement il y a une correspondance 1-1 entre eux.
Callum Rogers

9
la conclusion, pour Haskell: -O2 lui donne environ 3x une accélération, et en utilisant Int au lieu d'Integer environ 4x-6x pour l'accélération totale de 12x-14x et plus.
Will Ness le

Réponses:


794

En utilisant GHC 7.0.3, gcc 4.4.6, Linux 2.6.29sur un x86_64 Core2 Duo (2.5GHz) la machine, la compilation en utilisant ghc -O2 -fllvm -fforce-recomppour Haskell et gcc -O3 -lmpour C.

  • Votre routine C s'exécute en 8,4 secondes (plus rapide que votre course probablement à cause de -O3)
  • La solution Haskell s'exécute en 36 secondes (en raison du -O2drapeau)
  • Votre factorCount'code n'est pas explicitement tapé et par défaut Integer(merci à Daniel d'avoir corrigé mon mauvais diagnostic ici!). Donner une signature de type explicite (ce qui est de toute façon une pratique standard) en utilisant Intet le temps passe à 11,1 secondes
  • en factorCount'vous avez inutilement appelé fromIntegral. Un correctif n'entraîne cependant aucun changement (le compilateur est intelligent, chanceux pour vous).
  • Vous avez utilisé modremest plus rapide et suffisant. Cela change le temps à 8,5 secondes .
  • factorCount'applique constamment deux arguments supplémentaires qui ne changent jamais ( number, sqrt). Une transformation travailleur / wrapper nous donne:
 $ time ./so
 842161320  

 real    0m7.954s  
 user    0m7.944s  
 sys     0m0.004s  

C'est vrai, 7,95 secondes . Constamment une demi - seconde plus rapide que la solution C . Sans le -fllvmdrapeau que j'obtiens toujours 8.182 seconds, le backend NCG se porte bien dans ce cas également.

Conclusion: Haskell est génial.

Code résultant

factorCount number = factorCount' number isquare 1 0 - (fromEnum $ square == fromIntegral isquare)
    where square = sqrt $ fromIntegral number
          isquare = floor square

factorCount' :: Int -> Int -> Int -> Int -> Int
factorCount' number sqrt candidate0 count0 = go candidate0 count0
  where
  go candidate count
    | candidate > sqrt = count
    | number `rem` candidate == 0 = go (candidate + 1) (count + 2)
    | otherwise = go (candidate + 1) count

nextTriangle index triangle
    | factorCount triangle > 1000 = triangle
    | otherwise = nextTriangle (index + 1) (triangle + index + 1)

main = print $ nextTriangle 1 1

EDIT: Alors maintenant que nous avons exploré cela, permet de répondre aux questions

Question 1: erlang, python et haskell perdent-ils de la vitesse en raison de l'utilisation d'entiers de longueur arbitraire ou pas tant que les valeurs sont inférieures à MAXINT?

Dans Haskell, l'utilisation Integerest plus lente que Intmais combien plus lente dépend des calculs effectués. Heureusement (pour les machines 64 bits) Intest suffisant. Pour des raisons de portabilité, vous devriez probablement réécrire mon code à utiliser Int64ou Word64(C n'est pas le seul langage avec a long).

Question 2: Pourquoi haskell est-il si lent? Existe-t-il un indicateur de compilation qui désactive les freins ou est-ce mon implémentation? (Ce dernier est tout à fait probable car haskell est un livre avec sept sceaux pour moi.)

Question 3: pouvez-vous me donner quelques conseils pour optimiser ces implémentations sans changer la façon dont je détermine les facteurs? Optimisation de toute façon: plus agréable, plus rapide, plus "natif" de la langue.

C'est ce que j'ai répondu plus haut. La réponse a été

  • 0) Utiliser l'optimisation via -O2
  • 1) Utilisez des types rapides (notamment: non compatibles) si possible
  • 2) remnon mod(une optimisation souvent oubliée) et
  • 3) transformation travailleur / wrapper (peut-être l'optimisation la plus courante).

Question 4: Mes implémentations fonctionnelles permettent-elles LCO et évitent donc d'ajouter des trames inutiles sur la pile d'appels?

Oui, ce n'était pas le problème. Bon travail et heureux que vous y ayez pensé.


25
@Karl Parce que remc'est en fait un sous-composant de l' modopération (ils ne sont pas les mêmes). Si vous regardez dans la bibliothèque GHC Base, vous voyez des modtests pour plusieurs conditions et ajustez le signe en conséquence. (voir modInt#en Base.lhs)
Thomas M. DuBuisson

20
Un autre point de données: j'ai écrit une traduction Haskell rapide du programme C sans regarder Haskell de @ Hyperboreus. C'est donc un peu plus proche de Haskell idiomatique standard, et la seule optimisation que j'ai ajoutée délibérément est de la remplacer modpar remaprès avoir lu cette réponse (hé, oups). Voir le lien pour mes timings, mais la version courte est "presque identique au C".
CA McCann

106
Même si la version C fonctionnait plus vite sur ma machine, j'ai maintenant un nouveau respect pour Haskell. +1
Seth Carnegie

11
C'est assez surprenant pour moi, même si je ne l'ai pas encore essayé. Étant donné que l'original factorCount'était récursif de queue, j'aurais pensé que le compilateur pouvait repérer les paramètres supplémentaires non modifiés et optimiser la récursivité de queue uniquement pour les paramètres changeants (Haskell étant un langage pur après tout, cela devrait être facile). Quelqu'un pense que le compilateur pourrait le faire ou devrais-je revenir pour lire plus d'articles théoriques?
kizzx2

22
@ kizzx2: Il y a un ticket GHC pour l'ajouter. D'après ce que j'ai compris, cette transformation peut entraîner des allocations supplémentaires d'objets de fermeture. Cela signifie des performances moins bonnes dans certains cas, mais comme Johan Tibell le suggère dans son article de blog, cela peut être évité si le wrapper résultant peut être intégré.
hammar

224

Il y a quelques problèmes avec l'implémentation d'Erlang. Comme référence pour les éléments suivants, mon temps d'exécution mesuré pour votre programme Erlang non modifié était de 47,6 secondes, contre 12,7 secondes pour le code C.

La première chose à faire si vous souhaitez exécuter du code Erlang intensif en calcul est d'utiliser du code natif. La compilation avec erlc +native euler12a réduit le temps à 41,3 secondes. Il s'agit cependant d'une accélération beaucoup plus faible (seulement 15%) que celle attendue de la compilation native sur ce type de code, et le problème est votre utilisation de -compile(export_all). Ceci est utile pour l'expérimentation, mais le fait que toutes les fonctions soient potentiellement accessibles de l'extérieur rend le compilateur natif très conservateur. (L'émulateur BEAM normal n'est pas tellement affecté.) Le remplacement de cette déclaration par -export([solve/0]).donne une bien meilleure accélération: 31,5 secondes (près de 35% de la ligne de base).

Mais le code lui-même a un problème: pour chaque itération dans la boucle factorCount, vous effectuez ce test:

factorCount (_, Sqrt, Candidate, Count) when Candidate == Sqrt -> Count + 1;

Le code C ne fait pas cela. En général, il peut être difficile de faire une comparaison équitable entre différentes implémentations du même code, et en particulier si l'algorithme est numérique, car vous devez être sûr qu'ils font réellement la même chose. Une légère erreur d'arrondi dans une implémentation due à un transtypage quelque part peut l'amener à effectuer beaucoup plus d'itérations que l'autre, même si les deux atteignent finalement le même résultat.

Pour éliminer cette source d'erreur possible (et se débarrasser du test supplémentaire à chaque itération), j'ai réécrit la fonction factorCount comme suit, étroitement modelée sur le code C:

factorCount (N) ->
    Sqrt = math:sqrt (N),
    ISqrt = trunc(Sqrt),
    if ISqrt == Sqrt -> factorCount (N, ISqrt, 1, -1);
       true          -> factorCount (N, ISqrt, 1, 0)
    end.

factorCount (_N, ISqrt, Candidate, Count) when Candidate > ISqrt -> Count;
factorCount ( N, ISqrt, Candidate, Count) ->
    case N rem Candidate of
        0 -> factorCount (N, ISqrt, Candidate + 1, Count + 2);
        _ -> factorCount (N, ISqrt, Candidate + 1, Count)
    end.

Cette réécriture, non export_all, et compilation native, m'a donné le temps d'exécution suivant:

$ erlc +native euler12.erl
$ time erl -noshell -s euler12 solve
842161320

real    0m19.468s
user    0m19.450s
sys 0m0.010s

ce qui n'est pas trop mal par rapport au code C:

$ time ./a.out 
842161320

real    0m12.755s
user    0m12.730s
sys 0m0.020s

considérant qu'Erlang n'est pas du tout orienté vers l'écriture de code numérique, étant seulement 50% plus lent que C sur un programme comme celui-ci, c'est plutôt bien.

Enfin, concernant vos questions:

Question 1: erlang, python et haskell perdent-ils de la vitesse en raison de l'utilisation d'entiers de longueur arbitraire ou pas tant que les valeurs sont inférieures à MAXINT?

Oui, un peu. Dans Erlang, il n'y a aucun moyen de dire "utiliser l'arithmétique 32/64 bits avec bouclage", donc à moins que le compilateur ne prouve certaines limites sur vos entiers (et ce n'est généralement pas le cas), il doit vérifier tous les calculs pour voir s'ils peuvent tenir dans un seul mot balisé ou s'il doit les transformer en bignums alloués en tas. Même si aucun bignum n'est jamais utilisé dans la pratique lors de l'exécution, ces vérifications devront être effectuées. D'un autre côté, cela signifie que vous savez que l'algorithme n'échouera jamais à cause d'un entourage entier inattendu si vous lui donnez soudainement des entrées plus grandes qu'auparavant.

Question 4: Mes implémentations fonctionnelles permettent-elles LCO et évitent donc d'ajouter des trames inutiles sur la pile d'appels?

Oui, votre code Erlang est correct en ce qui concerne l'optimisation du dernier appel.


2
Je suis d'accord avec toi. Ce repère n'était pas précis en particulier pour Erlang pour un certain nombre de raisons
Muzaaya Joshua

156

En ce qui concerne l'optimisation de Python, en plus d'utiliser PyPy (pour des accélérations assez impressionnantes sans modification de votre code), vous pouvez utiliser la chaîne d' outils de traduction de PyPy pour compiler une version compatible RPython, ou Cython pour construire un module d'extension, les deux de qui sont plus rapides que la version C dans mes tests, avec le module Cython presque deux fois plus rapide . Pour référence, j'inclus également les résultats de référence C et PyPy:

C (compilé avec gcc -O3 -lm)

% time ./euler12-c 
842161320

./euler12-c  11.95s 
 user 0.00s 
 system 99% 
 cpu 11.959 total

PyPy 1.5

% time pypy euler12.py
842161320
pypy euler12.py  
16.44s user 
0.01s system 
99% cpu 16.449 total

RPython (en utilisant la dernière révision de PyPy, c2f583445aee)

% time ./euler12-rpython-c
842161320
./euler12-rpy-c  
10.54s user 0.00s 
system 99% 
cpu 10.540 total

Cython 0,15

% time python euler12-cython.py
842161320
python euler12-cython.py  
6.27s user 0.00s 
system 99% 
cpu 6.274 total

La version RPython a quelques changements clés. Pour traduire en un programme autonome, vous devez définir votre target, qui est dans ce cas la mainfonction. Il est censé accepter sys.argvcomme seul argument et doit renvoyer un entier. Vous pouvez le traduire en utilisant translate.py, % translate.py euler12-rpython.pyqui se traduit en C et le compile pour vous.

# euler12-rpython.py

import math, sys

def factorCount(n):
    square = math.sqrt(n)
    isquare = int(square)
    count = -1 if isquare == square else 0
    for candidate in xrange(1, isquare + 1):
        if not n % candidate: count += 2
    return count

def main(argv):
    triangle = 1
    index = 1
    while factorCount(triangle) < 1001:
        index += 1
        triangle += index
    print triangle
    return 0

if __name__ == '__main__':
    main(sys.argv)

def target(*args):
    return main, None

La version Cython a été réécrite en tant que module d'extension _euler12.pyx, que j'importe et appelle à partir d'un fichier python normal. Le _euler12.pyxest essentiellement le même que votre version, avec quelques déclarations de type statique supplémentaires. Le setup.py a le passe-partout normal pour construire l'extension, en utilisant python setup.py build_ext --inplace.

# _euler12.pyx
from libc.math cimport sqrt

cdef int factorCount(int n):
    cdef int candidate, isquare, count
    cdef double square
    square = sqrt(n)
    isquare = int(square)
    count = -1 if isquare == square else 0
    for candidate in range(1, isquare + 1):
        if not n % candidate: count += 2
    return count

cpdef main():
    cdef int triangle = 1, index = 1
    while factorCount(triangle) < 1001:
        index += 1
        triangle += index
    print triangle

# euler12-cython.py
import _euler12
_euler12.main()

# setup.py
from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext

ext_modules = [Extension("_euler12", ["_euler12.pyx"])]

setup(
  name = 'Euler12-Cython',
  cmdclass = {'build_ext': build_ext},
  ext_modules = ext_modules
)

Honnêtement, j'ai très peu d'expérience avec RPython ou Cython, et j'ai été agréablement surpris des résultats. Si vous utilisez CPython, l'écriture de vos bits de code gourmands en CPU dans un module d'extension Cython semble être un moyen très simple d'optimiser votre programme.


6
Je suis curieux, la version C peut-elle être optimisée pour être au moins aussi rapide que CPython?
Afficher le nom le

4
@SargeBorsch que la version Cython est si rapide, car elle se compile en une source C hautement optimisée, ce qui signifie que vous pouvez certainement obtenir ces performances de C.
Eli Korvigo

72

Question 3: Pouvez-vous me donner quelques conseils pour optimiser ces implémentations sans changer la façon dont je détermine les facteurs? Optimisation de toute façon: plus agréable, plus rapide, plus "natif" de la langue.

L'implémentation C est sous-optimale (comme le suggère Thomas M. DuBuisson), la version utilise des entiers 64 bits (c'est-à-dire un type de données long ). J'examinerai la liste des assembleurs plus tard, mais avec une supposition éclairée, il y a des accès à la mémoire dans le code compilé, ce qui rend l'utilisation des entiers 64 bits beaucoup plus lente. C'est cela ou le code généré (que ce soit le fait que vous pouvez tenir moins d'entiers 64 bits dans un registre SSE ou arrondir un double à un entier 64 bits est plus lent).

Voici le code modifié (remplacez simplement long par int et j'ai explicitement inclus factorCount, bien que je ne pense pas que cela soit nécessaire avec gcc -O3):

#include <stdio.h>
#include <math.h>

static inline int factorCount(int n)
{
    double square = sqrt (n);
    int isquare = (int)square;
    int count = isquare == square ? -1 : 0;
    int candidate;
    for (candidate = 1; candidate <= isquare; candidate ++)
        if (0 == n % candidate) count += 2;
    return count;
}

int main ()
{
    int triangle = 1;
    int index = 1;
    while (factorCount (triangle) < 1001)
    {
        index++;
        triangle += index;
    }
    printf ("%d\n", triangle);
}

Running + timing ça donne:

$ gcc -O3 -lm -o euler12 euler12.c; time ./euler12
842161320
./euler12  2.95s user 0.00s system 99% cpu 2.956 total

Pour référence, l'implémentation de haskell par Thomas dans la réponse précédente donne:

$ ghc -O2 -fllvm -fforce-recomp euler12.hs; time ./euler12                                                                                      [9:40]
[1 of 1] Compiling Main             ( euler12.hs, euler12.o )
Linking euler12 ...
842161320
./euler12  9.43s user 0.13s system 99% cpu 9.602 total

Conclusion: ne rien enlever à ghc, c'est un excellent compilateur, mais gcc génère normalement du code plus rapide.


22
Très agréable! À titre de comparaison, sur ma machine, votre solution C s'exécute 2.5 secondstandis qu'une modification similaire au code Haskell (passage à Word32, ajout de pragma INLINE) entraîne un runtime de 4.8 seconds. Peut-être que quelque chose peut être fait (pas banalement, semble-t-il) - le résultat gcc est certainement impressionnant.
Thomas M. DuBuisson

1
Merci! La question devrait peut-être être la vitesse de sortie compilée par divers compilateurs plutôt que le langage lui-même. Là encore, retirer les manuels Intel et l'optimiser à la main gagnera toujours carrément (à condition que vous ayez les connaissances et le temps (beaucoup)).
Raedwulf

56

Jetez un œil à ce blog . Au cours de la dernière année, il a résolu quelques-uns des problèmes de Project Euler à Haskell et Python, et il a généralement trouvé que Haskell était beaucoup plus rapide. Je pense qu'entre ces langues, cela a plus à voir avec votre aisance et votre style de codage.

En ce qui concerne la vitesse Python, vous utilisez la mauvaise implémentation! Essayez PyPy , et pour des choses comme ça, vous le trouverez beaucoup, beaucoup plus rapide.


32

Votre implémentation Haskell pourrait être considérablement accélérée en utilisant certaines fonctions des packages Haskell. Dans ce cas, j'ai utilisé des nombres premiers, qui sont juste installés avec 'cabal install prime';)

import Data.Numbers.Primes
import Data.List

triangleNumbers = scanl1 (+) [1..]
nDivisors n = product $ map ((+1) . length) (group (primeFactors n))
answer = head $ filter ((> 500) . nDivisors) triangleNumbers

main :: IO ()
main = putStrLn $ "First triangle number to have over 500 divisors: " ++ (show answer)

Calendrier:

Votre programme d'origine:

PS> measure-command { bin\012_slow.exe }

TotalSeconds      : 16.3807409
TotalMilliseconds : 16380.7409

Amélioration de la mise en œuvre

PS> measure-command { bin\012.exe }

TotalSeconds      : 0.0383436
TotalMilliseconds : 38.3436

Comme vous pouvez le voir, celui-ci fonctionne en 38 millisecondes sur la même machine où la vôtre a fonctionné en 16 secondes :)

Commandes de compilation:

ghc -O2 012.hs -o bin\012.exe
ghc -O2 012_slow.hs -o bin\012_slow.exe

5
La dernière fois que j'ai vérifié que les "nombres premiers" de Haskell n'étaient qu'une énorme liste de nombres premiers précalculés - pas de calcul, juste une recherche. Alors oui, bien sûr, ce sera plus rapide, mais cela ne vous dit rien sur la vitesse de calcul de la dérivation des nombres premiers dans Haskell.
zxq9

21
@ zxq9 pourriez-vous m'indiquer où se trouve dans la source du package prime ( hackage.haskell.org/package/primes-0.2.1.0/docs/src/… ) la liste des nombres premiers?
Fraser

4
Alors que la source montre que les nombres premiers ne sont pas précalculés, cette accélération est complètement folle, des kilomètres plus rapide que la version C, alors qu'est-ce qui se passe?
point

1
Mémorisation @semicolon. Dans ce cas, je pense que Haskell a mémorisé tous les nombres premiers à l'exécution, il n'a donc pas besoin de les recalculer à chaque itération.
Hauleth

5
C'est 1000 diviseurs, pas 500.
Casper Færgemand

29

Juste pour le fun. Voici une implémentation Haskell plus «native»:

import Control.Applicative
import Control.Monad
import Data.Either
import Math.NumberTheory.Powers.Squares

isInt :: RealFrac c => c -> Bool
isInt = (==) <$> id <*> fromInteger . round

intSqrt :: (Integral a) => a -> Int
--intSqrt = fromIntegral . floor . sqrt . fromIntegral
intSqrt = fromIntegral . integerSquareRoot'

factorize :: Int -> [Int]
factorize 1 = []
factorize n = first : factorize (quot n first)
  where first = (!! 0) $ [a | a <- [2..intSqrt n], rem n a == 0] ++ [n]

factorize2 :: Int -> [(Int,Int)]
factorize2 = foldl (\ls@((val,freq):xs) y -> if val == y then (val,freq+1):xs else (y,1):ls) [(0,0)] . factorize

numDivisors :: Int -> Int
numDivisors = foldl (\acc (_,y) -> acc * (y+1)) 1 <$> factorize2

nextTriangleNumber :: (Int,Int) -> (Int,Int)
nextTriangleNumber (n,acc) = (n+1,acc+n+1)

forward :: Int -> (Int, Int) -> Either (Int, Int) (Int, Int)
forward k val@(n,acc) = if numDivisors acc > k then Left val else Right (nextTriangleNumber val)

problem12 :: Int -> (Int, Int)
problem12 n = (!!0) . lefts . scanl (>>=) (forward n (1,1)) . repeat . forward $ n

main = do
  let (n,val) = problem12 1000
  print val

En utilisant ghc -O3 , cela fonctionne régulièrement en 0,55-0,58 secondes sur ma machine (1,73 GHz Core i7).

Une fonction factorCount plus efficace pour la version C:

int factorCount (int n)
{
  int count = 1;
  int candidate,tmpCount;
  while (n % 2 == 0) {
    count++;
    n /= 2;
  }
    for (candidate = 3; candidate < n && candidate * candidate < n; candidate += 2)
    if (n % candidate == 0) {
      tmpCount = 1;
      do {
        tmpCount++;
        n /= candidate;
      } while (n % candidate == 0);
       count*=tmpCount;
      }
  if (n > 1)
    count *= 2;
  return count;
}

Changer les longs en pouces en principal, en utilisant gcc -O3 -lm, cela s'exécute régulièrement en 0,31-0,35 secondes.

Les deux peuvent être exécutés encore plus rapidement si vous tirez parti du fait que le nième nombre de triangle = n * (n + 1) / 2, et n et (n + 1) ont des factorisations premières complètement disparates, donc le nombre de facteurs de chaque moitié peut être multiplié pour trouver le nombre de facteurs de l'ensemble. Le suivant:

int main ()
{
  int triangle = 0,count1,count2 = 1;
  do {
    count1 = count2;
    count2 = ++triangle % 2 == 0 ? factorCount(triangle+1) : factorCount((triangle+1)/2);
  } while (count1*count2 < 1001);
  printf ("%lld\n", ((long long)triangle)*(triangle+1)/2);
}

réduira le temps d'exécution du code c à 0,17-0,19 secondes, et il peut gérer des recherches beaucoup plus importantes - plus de 10000 facteurs prennent environ 43 secondes sur ma machine. Je laisse une accélération de haskell similaire au lecteur intéressé.


3
Juste pour comparaison: version c originale: 9.1690, version thaumkid: 0.1060 86x amélioration.
merci le

Sensationnel. Haskell fonctionne très bien une fois que vous évitez les types inférés
Piyush Katariya

En fait, ce n'est pas l'inférence qui l'a fait. Cela vous aide uniquement A) à déboguer ou à éviter les problèmes de type et les problèmes de sélection d'instance de classe B) à déboguer et à éviter certains problèmes de type indécidables avec quelques extensions de langage modernes. Il vous aide également à rendre vos programmes incontrôlables afin que vous ne puissiez jamais augmenter vos efforts de développement.
codeshot

c version 0.11 s sur Intel skull canyon
codeshot

13
Question 1: erlang, python et haskell perdent-ils de la vitesse en raison de l'utilisation d'entiers de longueur arbitraire ou pas tant que les valeurs sont inférieures à MAXINT?

C'est peu probable. Je ne peux pas en dire beaucoup sur Erlang et Haskell (enfin, peut-être un peu sur Haskell ci-dessous) mais je peux signaler beaucoup d'autres goulots d'étranglement en Python. Chaque fois que le programme essaie d'exécuter une opération avec certaines valeurs en Python, il doit vérifier si les valeurs sont du type approprié et cela coûte un peu de temps. Votre factorCountfonction alloue simplement une liste à range (1, isquare + 1)différents moments, et l' mallocallocation de mémoire à l' exécution est beaucoup plus lente que l'itération sur une plage avec un compteur comme vous le faites en C. Notamment, lefactorCount() est appelée plusieurs fois et alloue donc beaucoup de listes. N'oublions pas non plus que Python est interprété et que l'interpréteur CPython n'a pas grand souci d'être optimisé.

EDIT : oh, eh bien, je note que vous utilisez Python 3 donc range()ne renvoie pas une liste, mais un générateur. Dans ce cas, mon point sur l'allocation des listes est à moitié faux: la fonction alloue simplement des rangeobjets, qui sont néanmoins inefficaces mais pas aussi inefficaces que l'allocation d'une liste avec beaucoup d'éléments.

Question 2: Pourquoi haskell est-il si lent? Existe-t-il un indicateur de compilation qui désactive les freins ou est-ce mon implémentation? (Ce dernier est tout à fait probable car haskell est un livre avec sept sceaux pour moi.)

Utilisez-vous des câlins ? Hugs est un interprète considérablement lent. Si vous l'utilisez, vous pouvez peut-être passer un meilleur moment avec GHC - mais je ne fais que cogiter l'hypotèse, le genre de choses qu'un bon compilateur Haskell fait sous le capot est assez fascinant et bien au-delà de ma compréhension :)

Question 3: pouvez-vous me donner quelques conseils pour optimiser ces implémentations sans changer la façon dont je détermine les facteurs? Optimisation de toute façon: plus agréable, plus rapide, plus "natif" de la langue.

Je dirais que vous jouez à un jeu drôle. La meilleure partie de la connaissance des différentes langues est de les utiliser de la manière la plus différente possible :) Mais je m'égare, je n'ai tout simplement aucune recommandation pour ce point. Désolé, j'espère que quelqu'un pourra vous aider dans ce cas :)

Question 4: Mes implémentations fonctionnelles permettent-elles LCO et évitent donc d'ajouter des trames inutiles sur la pile d'appels?

Pour autant que je m'en souvienne, il vous suffit de vous assurer que votre appel récursif est la dernière commande avant de renvoyer une valeur. En d'autres termes, une fonction comme celle ci-dessous pourrait utiliser une telle optimisation:

def factorial(n, acc=1):
    if n > 1:
        acc = acc * n
        n = n - 1
        return factorial(n, acc)
    else:
        return acc

Cependant, vous n'auriez pas une telle optimisation si votre fonction était comme celle ci-dessous, car il y a une opération (multiplication) après l'appel récursif:

def factorial2(n):
    if n > 1:
        f = factorial2(n-1)
        return f*n
    else:
        return 1

J'ai séparé les opérations dans certaines variables locales pour indiquer clairement quelles opérations sont exécutées. Cependant, le plus habituel est de voir ces fonctions comme ci-dessous, mais elles sont équivalentes pour le point que je fais:

def factorial(n, acc=1):
    if n > 1:
        return factorial(n-1, acc*n)
    else:
        return acc

def factorial2(n):
    if n > 1:
        return n*factorial(n-1)
    else:
        return 1

Notez qu'il appartient au compilateur / interprète de décider s'il effectuera la récursivité de la queue. Par exemple, l'interpréteur Python ne le fait pas si je me souviens bien (j'ai utilisé Python dans mon exemple uniquement en raison de sa syntaxe fluide). Quoi qu'il en soit, si vous trouvez des choses étranges telles que des fonctions factorielles avec deux paramètres (et l'un des paramètres a des noms tels que acc, accumulatoretc.), vous savez maintenant pourquoi les gens le font :)


@Hyperboreus merci! Aussi, je suis vraiment curieux de vos prochaines questions. Cependant, je vous préviens que mes connaissances sont limitées et je n'ai donc pas pu répondre à toutes vos questions. Pour essayer de le compenser, j'ai créé mon wiki de communauté de réponse pour que les gens puissent le compléter plus facilement.
brandizzi

À propos de l'utilisation de la plage. Lorsque je remplace la plage par une boucle while avec incrément (imitant la boucle for de C), le temps d'exécution double en fait. Je suppose que les générateurs sont assez optimisés.
Hyperboreus

12

Avec Haskell, vous n'avez vraiment pas besoin de penser explicitement aux récursions.

factorCount number = foldr factorCount' 0 [1..isquare] -
                     (fromEnum $ square == fromIntegral isquare)
    where
      square = sqrt $ fromIntegral number
      isquare = floor square
      factorCount' candidate
        | number `rem` candidate == 0 = (2 +)
        | otherwise = id

triangles :: [Int]
triangles = scanl1 (+) [1,2..]

main = print . head $ dropWhile ((< 1001) . factorCount) triangles

Dans le code ci-dessus, j'ai remplacé les récursions explicites dans la réponse de @Thomas par des opérations de liste courantes. Le code fait toujours exactement la même chose sans nous soucier de la récursivité de la queue. Il s'exécute (~ 7.49s ) environ 6% plus lentement que la version dans la réponse de @Thomas (~ 7.04s ) sur ma machine avec GHC 7.6.2, tandis que la version C de @Raedwulf fonctionne ~ 3.15s . Il semble que le GHC s'est amélioré au cours de l'année.

PS. Je sais que c'est une vieille question, et je tombe dessus à partir de recherches Google (j'ai oublié ce que je cherchais, maintenant ...). Je voulais juste commenter la question sur LCO et exprimer mes sentiments à propos de Haskell en général. Je voulais commenter la première réponse, mais les commentaires n'autorisent pas les blocs de code.


9

Quelques chiffres et explications supplémentaires pour la version C. Apparemment, personne ne l'a fait pendant toutes ces années. N'oubliez pas de voter pour cette réponse afin qu'elle puisse être visible et apprise par tout le monde.

Première étape: analyse comparative des programmes de l'auteur

Spécifications de l'ordinateur portable:

  • CPU i3 M380 (931 MHz - mode d'économie de batterie maximum)
  • 4 Go de mémoire
  • Win7 64 bits
  • Microsoft Visual Studio 2012 Ultimate
  • Cygwin avec gcc 4.9.3
  • Python 2.7.10

Commandes:

compiling on VS x64 command prompt > `for /f %f in ('dir /b *.c') do cl /O2 /Ot /Ox %f -o %f_x64_vs2012.exe`
compiling on cygwin with gcc x64   > `for f in ./*.c; do gcc -m64 -O3 $f -o ${f}_x64_gcc.exe ; done`
time (unix tools) using cygwin > `for f in ./*.exe; do  echo "----------"; echo $f ; time $f ; done`

.

----------
$ time python ./original.py

real    2m17.748s
user    2m15.783s
sys     0m0.093s
----------
$ time ./original_x86_vs2012.exe

real    0m8.377s
user    0m0.015s
sys     0m0.000s
----------
$ time ./original_x64_vs2012.exe

real    0m8.408s
user    0m0.000s
sys     0m0.015s
----------
$ time ./original_x64_gcc.exe

real    0m20.951s
user    0m20.732s
sys     0m0.030s

Les noms de fichiers sont: integertype_architecture_compiler.exe

  • integertype est le même que le programme d'origine pour l'instant (plus à ce sujet plus tard)
  • l'architecture est x86 ou x64 selon les paramètres du compilateur
  • le compilateur est gcc ou vs2012

Deuxième étape: enquêter, améliorer et comparer à nouveau

VS est 250% plus rapide que gcc. Les deux compilateurs devraient donner une vitesse similaire. De toute évidence, quelque chose ne va pas avec le code ou les options du compilateur. Cherchons!

Le premier point d'intérêt est les types entiers. Les conversions peuvent être coûteuses et la cohérence est importante pour une meilleure génération de code et optimisations. Tous les entiers doivent être du même type.

C'est un désordre mixte de intet en longce moment. Nous allons améliorer cela. Quel type utiliser? Le plus rapide. Je dois tous les comparer!

----------
$ time ./int_x86_vs2012.exe

real    0m8.440s
user    0m0.016s
sys     0m0.015s
----------
$ time ./int_x64_vs2012.exe

real    0m8.408s
user    0m0.016s
sys     0m0.015s
----------
$ time ./int32_x86_vs2012.exe

real    0m8.408s
user    0m0.000s
sys     0m0.015s
----------
$ time ./int32_x64_vs2012.exe

real    0m8.362s
user    0m0.000s
sys     0m0.015s
----------
$ time ./int64_x86_vs2012.exe

real    0m18.112s
user    0m0.000s
sys     0m0.015s
----------
$ time ./int64_x64_vs2012.exe

real    0m18.611s
user    0m0.000s
sys     0m0.015s
----------
$ time ./long_x86_vs2012.exe

real    0m8.393s
user    0m0.015s
sys     0m0.000s
----------
$ time ./long_x64_vs2012.exe

real    0m8.440s
user    0m0.000s
sys     0m0.015s
----------
$ time ./uint32_x86_vs2012.exe

real    0m8.362s
user    0m0.000s
sys     0m0.015s
----------
$ time ./uint32_x64_vs2012.exe

real    0m8.393s
user    0m0.015s
sys     0m0.015s
----------
$ time ./uint64_x86_vs2012.exe

real    0m15.428s
user    0m0.000s
sys     0m0.015s
----------
$ time ./uint64_x64_vs2012.exe

real    0m15.725s
user    0m0.015s
sys     0m0.015s
----------
$ time ./int_x64_gcc.exe

real    0m8.531s
user    0m8.329s
sys     0m0.015s
----------
$ time ./int32_x64_gcc.exe

real    0m8.471s
user    0m8.345s
sys     0m0.000s
----------
$ time ./int64_x64_gcc.exe

real    0m20.264s
user    0m20.186s
sys     0m0.015s
----------
$ time ./long_x64_gcc.exe

real    0m20.935s
user    0m20.809s
sys     0m0.015s
----------
$ time ./uint32_x64_gcc.exe

real    0m8.393s
user    0m8.346s
sys     0m0.015s
----------
$ time ./uint64_x64_gcc.exe

real    0m16.973s
user    0m16.879s
sys     0m0.030s

Les types entiers sont int long int32_t uint32_t int64_tet uint64_tde#include <stdint.h>

Il y a BEAUCOUP de types entiers en C, plus certains signés / non signés pour jouer avec, plus le choix de compiler en x86 ou x64 (à ne pas confondre avec la taille entière réelle). C'est beaucoup de versions à compiler et à exécuter ^^

Troisième étape: comprendre les chiffres

Conclusions définitives:

  • Les entiers 32 bits sont ~ 200% plus rapides que les équivalents 64 bits
  • les entiers 64 bits non signés sont 25% plus rapides que les 64 bits signés (malheureusement, je n'ai aucune explication à cela)

Question piège: "Quelles sont les tailles d'int et de long en C?"
La bonne réponse est: la taille de int et long en C ne sont pas bien définies!

De la spécification C:

int est d'au moins 32 bits de
long est au moins un int

Depuis la page de manuel de gcc (indicateurs -m32 et -m64):

L'environnement 32 bits définit int, long et pointeur sur 32 bits et génère du code qui s'exécute sur n'importe quel système i386.
L'environnement 64 bits définit int sur 32 bits et long et le pointeur sur 64 bits et génère du code pour l'architecture x86-64 d'AMD.

De la documentation MSDN (plages de types de données) https://msdn.microsoft.com/en-us/library/s3f49ktz%28v=vs.110%29.aspx :

int, 4 octets, également connu comme
long signé , 4 octets, également connu comme long int et signé long int

Pour conclure: leçons apprises

  • Les entiers 32 bits sont plus rapides que les entiers 64 bits.

  • Les types d'entiers standard ne sont pas bien définis en C ni en C ++, ils varient en fonction des compilateurs et des architectures. Lorsque vous avez besoin de cohérence et de prévisibilité, utilisez la uint32_tfamille entière de #include <stdint.h>.

  • Problèmes de vitesse résolus. Toutes les autres langues sont de retour à des centaines de pour cent, C & C ++ gagne à nouveau! Ils le font toujours. La prochaine amélioration sera le multithreading en utilisant OpenMP: D


Par curiosité, comment font les compilateurs Intel? Ils sont généralement très bons pour optimiser le code numérique.
kirbyfan64sos

Où trouvez-vous une référence indiquant que la spécification C garantit "int est au moins 32 bits"? Les seules garanties que je connaisse sont les amplitudes minimales de INT_MINet INT_MAX(-32767 et 32767, qui imposent pratiquement une exigence d' intau moins 16 bits). longdoit être au moins aussi grand qu'un int, et la moyenne des exigences de plage longest d'au moins 32 bits.
ShadowRanger


8

En regardant votre implémentation Erlang. Le timing a inclus le démarrage de la machine virtuelle entière, l'exécution de votre programme et l'arrêt de la machine virtuelle. Suis assez sûr que la mise en place et l'arrêt du erlang vm prend un certain temps.

Si le chronométrage était effectué au sein de la machine virtuelle erlang elle-même, les résultats seraient différents car dans ce cas, nous n'aurions l'heure réelle que pour le programme en question. Sinon, je crois que le temps total pris par le processus de démarrage et de chargement de l'Erlang Vm plus celui de son arrêt (comme vous le mettez dans votre programme) sont tous inclus dans le temps total que la méthode que vous utilisez pour chronométrer le programme sort. Pensez à utiliser le timing erlang lui-même que nous utilisons lorsque nous voulons synchroniser nos programmes au sein de la machine virtuelle elle-même timer:tc/1 or timer:tc/2 or timer:tc/3. De cette façon, les résultats d'erlang excluront le temps nécessaire pour démarrer et arrêter / tuer / arrêter la machine virtuelle. C'est mon raisonnement là-bas, pensez-y, puis essayez à nouveau votre repère.

Je suggère en fait que nous essayions de chronométrer le programme (pour les langues qui ont un runtime), dans le temps d'exécution de ces langues afin d'obtenir une valeur précise. C, par exemple, n'a pas de frais généraux pour démarrer et arrêter un système d'exécution comme Erlang, Python et Haskell (98% sûr de cela - je corrige). Donc (sur la base de ce raisonnement), je conclus en disant que ce benchmark n'était pas suffisamment précis / juste pour les langues fonctionnant au-dessus d'un système d'exécution. Permet de recommencer avec ces changements.

EDIT: en outre, même si toutes les langues avaient des systèmes d'exécution, la surcharge de démarrage et d'arrêt de chacun différerait. donc je suggère que nous chronométrions à partir des systèmes d'exécution (pour les langues pour lesquelles cela s'applique). La VM Erlang est connue pour avoir des frais généraux considérables au démarrage!


J'ai oublié de le mentionner dans mon article, mais j'ai mesuré le temps qu'il faut pour démarrer le système (erl -noshell -s erlang stop) - environ 0,1 seconde sur ma machine. C'est assez petit par rapport à la durée d'exécution du programme (environ 10 secondes) qu'il ne vaut pas la peine de discuter.
RichardC

sur votre machine! nous ne savons pas si vous travaillez sur un serveur Sun Fire !. Étant donné que le temps est une variable proportionnelle aux spécifications de la machine, il convient de la prendre en considération.
Muzaaya Joshua

2
@RichardC Nulle part mentionné qu'Erlang est plus rapide :) Il a des objectifs différents, pas de vitesse!
Exception

7

Question 1: Erlang, Python et Haskell perdent-ils de la vitesse en raison de l'utilisation d'entiers de longueur arbitraire ou pas tant que les valeurs sont inférieures à MAXINT?

La première question peut recevoir une réponse négative pour Erlang. On répond à la dernière question en utilisant Erlang de manière appropriée, comme dans:

http://bredsaal.dk/learning-erlang-using-projecteuler-net

Comme il est plus rapide que votre exemple C initial, je suppose qu'il existe de nombreux problèmes, car d'autres ont déjà été traités en détail.

Ce module Erlang s'exécute sur un netbook bon marché en environ 5 secondes ... Il utilise le modèle de threads réseau dans erlang et, en tant que tel, montre comment tirer parti du modèle d'événement. Il pourrait être distribué sur de nombreux nœuds. Et c'est rapide. Pas mon code.

-module(p12dist).  
-author("Jannich Brendle, jannich@bredsaal.dk, http://blog.bredsaal.dk").  
-compile(export_all).

server() ->  
  server(1).

server(Number) ->  
  receive {getwork, Worker_PID} -> Worker_PID ! {work,Number,Number+100},  
  server(Number+101);  
  {result,T} -> io:format("The result is: \~w.\~n", [T]);  
  _ -> server(Number)  
  end.

worker(Server_PID) ->  
  Server_PID ! {getwork, self()},  
  receive {work,Start,End} -> solve(Start,End,Server_PID)  
  end,  
  worker(Server_PID).

start() ->  
  Server_PID = spawn(p12dist, server, []),  
  spawn(p12dist, worker, [Server_PID]),  
  spawn(p12dist, worker, [Server_PID]),  
  spawn(p12dist, worker, [Server_PID]),  
  spawn(p12dist, worker, [Server_PID]).

solve(N,End,_) when N =:= End -> no_solution;

solve(N,End,Server_PID) ->  
  T=round(N*(N+1)/2),
  case (divisor(T,round(math:sqrt(T))) > 500) of  
    true ->  
      Server_PID ! {result,T};  
    false ->  
      solve(N+1,End,Server_PID)  
  end.

divisors(N) ->  
  divisor(N,round(math:sqrt(N))).

divisor(_,0) -> 1;  
divisor(N,I) ->  
  case (N rem I) =:= 0 of  
  true ->  
    2+divisor(N,I-1);  
  false ->  
    divisor(N,I-1)  
  end.

Le test ci-dessous a eu lieu sur: un processeur Intel (R) Atom (TM) N270 à 1,60 GHz

~$ time erl -noshell -s p12dist start

The result is: 76576500.

^C

BREAK: (a)bort (c)ontinue (p)roc info (i)nfo (l)oaded
       (v)ersion (k)ill (D)b-tables (d)istribution
a

real    0m5.510s
user    0m5.836s
sys 0m0.152s

augmenter la valeur à 1000 comme ci-dessous n'obtient pas le résultat correct. Avec> 500 comme ci-dessus, test le plus récent: IntelCore2 CPU 6600 @ 2.40GHz comletes in real 0m2.370s
Mark Washeim

votre résultat: 76576500 tout le monde: 842161320 il y a quelque chose de bizarre avec votre résultat
davidDavidson

Depuis que je rencontrais d'autres problèmes avec Euler, je viens de vérifier mon résultat. La réponse à projecteuler.net/problem=12 est 76576500 sans aucun doute à ce sujet. Je sais que cela semble étrange, mais je viens de vérifier.
Mark Washeim

À titre de comparaison, j'obtiens 9.03 avec la version c d'origine en utilisant Erlang 19 avec le code de Mark, j'obtiens 5.406, 167.0366% plus rapide.
merci le

5

C ++ 11, <20 ms pour moi - Exécutez-le ici

Je comprends que vous vouliez des conseils pour améliorer vos connaissances spécifiques à la langue, mais comme cela a été bien couvert ici, j'ai pensé ajouter un peu de contexte pour les personnes qui auraient pu lire le commentaire mathématique sur votre question, etc., et je me suis demandé pourquoi cela le code était tellement plus lent.

Cette réponse est principalement destinée à fournir un contexte pour, espérons-le, aider les gens à évaluer le code dans votre question / autres réponses plus facilement.

Ce code utilise seulement quelques optimisations (laides), sans rapport avec le langage utilisé, basées sur:

  1. chaque numéro de singularité est de la forme n (n + 1) / 2
  2. n et n + 1 sont des nombres premiers
  3. le nombre de diviseurs est une fonction multiplicative

#include <iostream>
#include <cmath>
#include <tuple>
#include <chrono>

using namespace std;

// Calculates the divisors of an integer by determining its prime factorisation.

int get_divisors(long long n)
{
    int divisors_count = 1;

    for(long long i = 2;
        i <= sqrt(n);
        /* empty */)
    {
        int divisions = 0;
        while(n % i == 0)
        {
            n /= i;
            divisions++;
        }

        divisors_count *= (divisions + 1);

        //here, we try to iterate more efficiently by skipping
        //obvious non-primes like 4, 6, etc
        if(i == 2)
            i++;
        else
            i += 2;
    }

    if(n != 1) //n is a prime
        return divisors_count * 2;
    else
        return divisors_count;
}

long long euler12()
{
    //n and n + 1
    long long n, n_p_1;

    n = 1; n_p_1 = 2;

    // divisors_x will store either the divisors of x or x/2
    // (the later iff x is divisible by two)
    long long divisors_n = 1;
    long long divisors_n_p_1 = 2;

    for(;;)
    {
        /* This loop has been unwound, so two iterations are completed at a time
         * n and n + 1 have no prime factors in common and therefore we can
         * calculate their divisors separately
         */

        long long total_divisors;                 //the divisors of the triangle number
                                                  // n(n+1)/2

        //the first (unwound) iteration

        divisors_n_p_1 = get_divisors(n_p_1 / 2); //here n+1 is even and we

        total_divisors =
                  divisors_n
                * divisors_n_p_1;

        if(total_divisors > 1000)
            break;

        //move n and n+1 forward
        n = n_p_1;
        n_p_1 = n + 1;

        //fix the divisors
        divisors_n = divisors_n_p_1;
        divisors_n_p_1 = get_divisors(n_p_1);   //n_p_1 is now odd!

        //now the second (unwound) iteration

        total_divisors =
                  divisors_n
                * divisors_n_p_1;

        if(total_divisors > 1000)
            break;

        //move n and n+1 forward
        n = n_p_1;
        n_p_1 = n + 1;

        //fix the divisors
        divisors_n = divisors_n_p_1;
        divisors_n_p_1 = get_divisors(n_p_1 / 2);   //n_p_1 is now even!
    }

    return (n * n_p_1) / 2;
}

int main()
{
    for(int i = 0; i < 1000; i++)
    {
        using namespace std::chrono;
        auto start = high_resolution_clock::now();
        auto result = euler12();
        auto end = high_resolution_clock::now();

        double time_elapsed = duration_cast<milliseconds>(end - start).count();

        cout << result << " " << time_elapsed << '\n';
    }
    return 0;
}

Cela prend environ 19 ms en moyenne pour mon ordinateur de bureau et 80 ms pour mon ordinateur portable, loin de la plupart des autres codes que j'ai vus ici. Et il y a, sans aucun doute, de nombreuses optimisations encore disponibles.


7
Ce n'est pas explicitement ce que le demandeur a demandé, "J'ai vraiment essayé d'implémenter le même algorithme aussi similaire que possible dans les quatre langues". Pour citer un commentaire sur l'une des nombreuses réponses supprimées similaires à la vôtre "il est assez évident que vous pouvez obtenir des vitesses plus rapides avec un meilleur algorithme quelle que soit la langue".
Thomas M. DuBuisson

2
@ ThomasM.DuBuisson. Voilà où je veux en venir. La question \ réponses implique fortement que les accélérations algorithmiques sont importantes (et bien sûr OP ne les demande pas), mais il n'y a pas d'exemple explicite. Je pense que cette réponse - qui n'est pas exactement du code fortement optimisé - fournit un peu de contexte utile à quiconque, comme moi, qui se demandait à quel point le code de l'OP était lent / rapide.
user3125280

gcc peut même pré-calculer de nombreux modèles. int a = 0; for (int i = 0; i <10000000; ++ i) {a + = i;} sera calculé au moment de la compilation, donc prenez <1 ms à l'exécution. Ça compte aussi
Arthur

@Thomas: Je dois être d'accord avec user3125280 - les langages doivent être comparés comment ils réussissent à faire quelque chose d' intelligent au lieu de comment ils ne parviennent pas à battre un vrai langage de programmation à faire quelque chose de stupide. Les algorithmes intelligents se soucient généralement moins de l'efficacité microscopique que de la flexibilité, de la possibilité de câbler (de les combiner) et de l'infrastructure. Le point n'est pas tant de savoir si on obtient 20 ms ou 50 ms, ce n'est pas 8 secondes ou 8 minutes.
DarthGizka

5

Essayer GO:

package main

import "fmt"
import "math"

func main() {
    var n, m, c int
    for i := 1; ; i++ {
        n, m, c = i * (i + 1) / 2, int(math.Sqrt(float64(n))), 0
        for f := 1; f < m; f++ {
            if n % f == 0 { c++ }
    }
    c *= 2
    if m * m == n { c ++ }
    if c > 1001 {
        fmt.Println(n)
        break
        }
    }
}

Je reçois:

version c originale: 9.1690 100%
go: 8.2520 111%

Mais en utilisant:

package main

import (
    "math"
    "fmt"
 )

// Sieve of Eratosthenes
func PrimesBelow(limit int) []int {
    switch {
        case limit < 2:
            return []int{}
        case limit == 2:
            return []int{2}
    }
    sievebound := (limit - 1) / 2
    sieve := make([]bool, sievebound+1)
    crosslimit := int(math.Sqrt(float64(limit))-1) / 2
    for i := 1; i <= crosslimit; i++ {
        if !sieve[i] {
            for j := 2 * i * (i + 1); j <= sievebound; j += 2*i + 1 {
                sieve[j] = true
            }
        }
    }
    plimit := int(1.3*float64(limit)) / int(math.Log(float64(limit)))
    primes := make([]int, plimit)
    p := 1
    primes[0] = 2
    for i := 1; i <= sievebound; i++ {
        if !sieve[i] {
            primes[p] = 2*i + 1
            p++
            if p >= plimit {
                break
            }
        }
    }
    last := len(primes) - 1
    for i := last; i > 0; i-- {
        if primes[i] != 0 {
            break
        }
        last = i
    }
    return primes[0:last]
}



func main() {
    fmt.Println(p12())
}
// Requires PrimesBelow from utils.go
func p12() int {
    n, dn, cnt := 3, 2, 0
    primearray := PrimesBelow(1000000)
    for cnt <= 1001 {
        n++
        n1 := n
        if n1%2 == 0 {
            n1 /= 2
        }
        dn1 := 1
        for i := 0; i < len(primearray); i++ {
            if primearray[i]*primearray[i] > n1 {
                dn1 *= 2
                break
            }
            exponent := 1
            for n1%primearray[i] == 0 {
                exponent++
                n1 /= primearray[i]
            }
            if exponent > 1 {
                dn1 *= exponent
            }
            if n1 == 1 {
                break
            }
        }
        cnt = dn * dn1
        dn = dn1
    }
    return n * (n - 1) / 2
}

Je reçois:

version c d'origine: 9,1690 100%
version c de thaumkid: 0,1060 8650%
version premier passage: 8,2520 111%
version deuxième passage: 0,0230 39865%

J'ai également essayé Python3.6 et pypy3.3-5.5-alpha:

version c originale: 8.629 100%
version c de thaumkid: 0.109 7916%
Python3.6: 54.795 16%
pypy3.3-5.5-alpha: 13.291 65%

puis avec le code suivant, j'ai:

version c originale: 8.629 100%
version c de thaumkid: 0.109 8650%
Python3.6: 1.489 580%
pypy3.3-5.5-alpha: 0.582 1483%

def D(N):
    if N == 1: return 1
    sqrtN = int(N ** 0.5)
    nf = 1
    for d in range(2, sqrtN + 1):
        if N % d == 0:
            nf = nf + 1
    return 2 * nf - (1 if sqrtN**2 == N else 0)

L = 1000
Dt, n = 0, 0

while Dt <= L:
    t = n * (n + 1) // 2
    Dt = D(n/2)*D(n+1) if n%2 == 0 else D(n)*D((n+1)/2)
    n = n + 1

print (t)

1

Changement: case (divisor(T,round(math:sqrt(T))) > 500) of

À: case (divisor(T,round(math:sqrt(T))) > 1000) of

Cela produira la bonne réponse pour l'exemple multi-processus d'Erlang.


2
Était-ce destiné à commenter cette réponse ? Parce que ce n'est pas clair, et ce n'est pas une réponse en soi.
ShadowRanger

1

J'ai fait l'hypothèse que le nombre de facteurs n'est important que si les nombres impliqués ont beaucoup de petits facteurs. J'ai donc utilisé l'excellent algorithme de thaumkid, mais j'ai d'abord utilisé une approximation du nombre de facteurs qui n'est jamais trop petite. C'est assez simple: vérifiez les facteurs premiers jusqu'à 29, puis vérifiez le nombre restant et calculez une limite supérieure pour le nombre de facteurs. Utilisez-le pour calculer une limite supérieure pour le nombre de facteurs, et si ce nombre est suffisamment élevé, calculez le nombre exact de facteurs.

Le code ci-dessous n'a pas besoin de cette hypothèse pour être correct, mais pour être rapide. Cela semble fonctionner; seulement environ un chiffre sur 100 000 donne une estimation suffisamment élevée pour nécessiter une vérification complète.

Voici le code:

// Return at least the number of factors of n.
static uint64_t approxfactorcount (uint64_t n)
{
    uint64_t count = 1, add;

#define CHECK(d)                            \
    do {                                    \
        if (n % d == 0) {                   \
            add = count;                    \
            do { n /= d; count += add; }    \
            while (n % d == 0);             \
        }                                   \
    } while (0)

    CHECK ( 2); CHECK ( 3); CHECK ( 5); CHECK ( 7); CHECK (11); CHECK (13);
    CHECK (17); CHECK (19); CHECK (23); CHECK (29);
    if (n == 1) return count;
    if (n < 1ull * 31 * 31) return count * 2;
    if (n < 1ull * 31 * 31 * 37) return count * 4;
    if (n < 1ull * 31 * 31 * 37 * 37) return count * 8;
    if (n < 1ull * 31 * 31 * 37 * 37 * 41) return count * 16;
    if (n < 1ull * 31 * 31 * 37 * 37 * 41 * 43) return count * 32;
    if (n < 1ull * 31 * 31 * 37 * 37 * 41 * 43 * 47) return count * 64;
    if (n < 1ull * 31 * 31 * 37 * 37 * 41 * 43 * 47 * 53) return count * 128;
    if (n < 1ull * 31 * 31 * 37 * 37 * 41 * 43 * 47 * 53 * 59) return count * 256;
    if (n < 1ull * 31 * 31 * 37 * 37 * 41 * 43 * 47 * 53 * 59 * 61) return count * 512;
    if (n < 1ull * 31 * 31 * 37 * 37 * 41 * 43 * 47 * 53 * 59 * 61 * 67) return count * 1024;
    if (n < 1ull * 31 * 31 * 37 * 37 * 41 * 43 * 47 * 53 * 59 * 61 * 67 * 71) return count * 2048;
    if (n < 1ull * 31 * 31 * 37 * 37 * 41 * 43 * 47 * 53 * 59 * 61 * 67 * 71 * 73) return count * 4096;
    return count * 1000000;
}

// Return the number of factors of n.
static uint64_t factorcount (uint64_t n)
{
    uint64_t count = 1, add;

    CHECK (2); CHECK (3);

    uint64_t d = 5, inc = 2;
    for (; d*d <= n; d += inc, inc = (6 - inc))
        CHECK (d);

    if (n > 1) count *= 2; // n must be a prime number
    return count;
}

// Prints triangular numbers with record numbers of factors.
static void printrecordnumbers (uint64_t limit)
{
    uint64_t record = 30000;

    uint64_t count1, factor1;
    uint64_t count2 = 1, factor2 = 1;

    for (uint64_t n = 1; n <= limit; ++n)
    {
        factor1 = factor2;
        count1 = count2;

        factor2 = n + 1; if (factor2 % 2 == 0) factor2 /= 2;
        count2 = approxfactorcount (factor2);

        if (count1 * count2 > record)
        {
            uint64_t factors = factorcount (factor1) * factorcount (factor2);
            if (factors > record)
            {
                printf ("%lluth triangular number = %llu has %llu factors\n", n, factor1 * factor2, factors);
                record = factors;
            }
        }
    }
}

On trouve ainsi le 14 753 024ème triangulaire avec 13824 facteurs en environ 0,7 seconde, le 879 207 715ème nombre triangulaire avec 61 440 facteurs en 34 secondes, le 12 524 486 975ème nombre triangulaire avec 138 240 facteurs en 10 minutes 5 secondes et le 26 467 792 064ème nombre triangulaire avec 172 032 facteurs en 21 minutes 25 secondes (2,4 GHz Core2 Duo), ce code ne prend donc que 116 cycles de processeur par numéro en moyenne. Le dernier nombre triangulaire lui-même est supérieur à 2 ^ 68, donc


0

J'ai modifié la version "Jannich Brendle" à 1000 au lieu de 500. Et liste le résultat de euler12.bin, euler12.erl, p12dist.erl. Les deux codes erl utilisent '+ native' pour compiler.

zhengs-MacBook-Pro:workspace zhengzhibin$ time erl -noshell -s p12dist start
The result is: 842161320.

real    0m3.879s
user    0m14.553s
sys     0m0.314s
zhengs-MacBook-Pro:workspace zhengzhibin$ time erl -noshell -s euler12 solve
842161320

real    0m10.125s
user    0m10.078s
sys     0m0.046s
zhengs-MacBook-Pro:workspace zhengzhibin$ time ./euler12.bin 
842161320

real    0m5.370s
user    0m5.328s
sys     0m0.004s
zhengs-MacBook-Pro:workspace zhengzhibin$

0
#include <stdio.h>
#include <math.h>

int factorCount (long n)
{
    double square = sqrt (n);
    int isquare = (int) square+1;
    long candidate = 2;
    int count = 1;
    while(candidate <= isquare && candidate<=n){
        int c = 1;
        while (n % candidate == 0) {
           c++;
           n /= candidate;
        }
        count *= c;
        candidate++;
    }
    return count;
}

int main ()
{
    long triangle = 1;
    int index = 1;
    while (factorCount (triangle) < 1001)
    {
        index ++;
        triangle += index;
    }
    printf ("%ld\n", triangle);
}

gcc -lm -Ofast euler.c

time ./a.out

2.79s utilisateur 0.00s système 99% cpu 2.794 au total

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.