Je n'avais pas prévu initialement d'écrire une réponse. Mais on m'a dit après qu'un autre utilisateur a fait l'étrange affirmation que la simple multiplication des premiers nombres premiers était plus coûteuse en calcul que l'application répétée lcm
. Voici donc les deux algorithmes et quelques repères:
Mon algorithme:
Algorithme de première génération, me donnant une liste infinie de nombres premiers.
isPrime :: Int -> Bool
isPrime 1 = False
isPrime n = all ((/= 0) . mod n) (takeWhile ((<= n) . (^ 2)) primes)
toPrime :: Int -> Int
toPrime n
| isPrime n = n
| otherwise = toPrime (n + 1)
primes :: [Int]
primes = 2 : map (toPrime . (+ 1)) primes
Maintenant, en utilisant cette liste principale pour calculer le résultat pour certains N
:
solvePrime :: Integer -> Integer
solvePrime n = foldl' (*) 1 $ takeWhile (<= n) (fromIntegral <$> primes)
Maintenant, l'autre algorithme basé sur lcm, qui est certes assez concis, principalement parce que j'ai implémenté la génération principale à partir de zéro (et que je n'ai pas utilisé l'algorithme de compréhension de liste super concise en raison de ses performances médiocres) alors qu'il lcm
était simplement importé de Prelude
.
solveLcm :: Integer -> Integer
solveLcm n = foldl' (flip lcm) 1 [2 .. n]
-- Much slower without `flip` on `lcm`
Maintenant pour les benchmarks, le code que j'ai utilisé pour chacun était simple: ( -prof -fprof-auto -O2
alors +RTS -p
)
main :: IO ()
main = print $ solvePrime n
-- OR
main = print $ solveLcm n
Pour n = 100,000
, solvePrime
:
total time = 0.04 secs
total alloc = 108,327,328 bytes
vs solveLcm
:
total time = 0.12 secs
total alloc = 117,842,152 bytes
Pour n = 1,000,000
, solvePrime
:
total time = 1.21 secs
total alloc = 8,846,768,456 bytes
vs solveLcm
:
total time = 9.10 secs
total alloc = 8,963,508,416 bytes
Pour n = 3,000,000
, solvePrime
:
total time = 8.99 secs
total alloc = 74,790,070,088 bytes
vs solveLcm
:
total time = 86.42 secs
total alloc = 75,145,302,416 bytes
je pense que les résultats parlent d'eux-mêmes.
Le profileur indique que la génération principale occupe un pourcentage de plus en plus petit du temps d'exécution à mesure que les n
augmentations. Ce n'est donc pas le goulot d'étranglement, nous pouvons donc l'ignorer pour l'instant.
Cela signifie que nous comparons vraiment l'appel lcm
où un argument va de 1 à n
, et l'autre va géométriquement de 1 à ans
. Pour appeler *
avec la même situation et l'avantage supplémentaire de pouvoir sauter tous les numéros non premiers (asymptotiquement gratuitement, en raison de la nature plus coûteuse de *
).
Et il est bien connu que *
c'est plus rapide que lcm
, comme cela lcm
nécessite des applications répétées de mod
, et mod
est asymptotiquement plus lent ( O(n^2)
vs ~O(n^1.5)
).
Ainsi, les résultats ci-dessus et la brève analyse de l'algorithme devraient rendre très évident quel algorithme est le plus rapide.