Mise à jour : Elixir n'est pas lent, mon algorithme l'était. Mes algorithmes n'étaient même pas des comparaisons de pommes à pommes. Voir les réponses de Roman ci-dessous pour les algorithmes équivalents à Ruby et Go. De plus, grâce à José, mon algorithme lent peut être considérablement accéléré en ajoutant simplement le préfixe MIX_ENV = prod. J'ai mis à jour les statistiques dans la question.
Question originale: Je travaille sur des problèmes de Project Euler dans plusieurs langues, juste pour voir à quel point les langues sont productives et rapides. Dans problème n ° 5 , on nous demande de trouver le plus petit nombre positif divisible par tous les nombres de 1 à 20.
J'ai implémenté la solution en plusieurs langues. Voici les statistiques:
Pourquoi la performance d'Elixir est-elle si lente? J'ai essayé d'utiliser les mêmes optimisations dans toutes les langues. Avertissement: je suis un novice FP et Elixir.
Puis-je faire quelque chose pour améliorer les performances dans Elixir? Si vous avez utilisé des outils de profilage pour trouver une meilleure solution, pourriez-vous les inclure dans la réponse?
En aller:
func problem005() int {
i := 20
outer:
for {
for j := 20; j > 0; j-- {
if i%j != 0 {
i = i + 20
continue outer
}
}
return i
}
panic("Should have found a solution by now")
}
En rubis:
def self.problem005
divisors = (1..20).to_a.reverse
number = 20 # we iterate over multiples of 20
until divisors.all? { |divisor| number % divisor == 0 } do
number += 20
end
return number
end
Dans Elixir:
def problem005 do
divisible_all? = fn num ->
Enum.all?((20..2), &(rem(num, &1) == 0))
end
Stream.iterate(20, &(&1 + 20))
|> Stream.filter(divisible_all?)
|> Enum.fetch! 0
end
Ma première réponse concernait l’implémentation du même algorithme que vous avez implémenté dans Ruby. Maintenant, voici une version dans Elixir de votre algorithme dans Go:
defmodule Euler do
@max_divider 20
def problem005 do
problem005(20, @max_divider)
end
defp problem005(number, divider) when divider > 1 do
if rem(number, divider) != 0 do
problem005(number+20, @max_divider)
else
problem005(number, divider-1)
end
end
defp problem005(number, _), do: number
end
Il faut environ 0,73s sur mon ordinateur portable. Ces algorithmes sont différents, donc je suis sûr que Ruby pourrait aussi mieux jouer ici.
Je suppose que la règle générale est la suivante: si vous avez du code dans Elixir dont les performances sont telles que 80% du code Go ou mieux, ce n'est pas grave. Dans d'autres cas, vous avez probablement une erreur d'algorithme dans votre code Elixir.
Mise à jour à propos de Ruby:
En bonus, voici l'algorithme équivalent Go en Ruby:
def problem_005
divisor = max_divisor = 20
number = 20 # we iterate over multiples of 20
while divisor > 1 do
if number % divisor == 0
divisor -= 1
else
number += 20
divisor = max_divisor
end
end
number
end
Il fonctionne 4,5 fois plus vite, donc je suppose qu’il pourrait afficher ~ 1,5 s sur votre ordinateur.
Essayez cette version:
defmodule Euler do
def problem005 do
problem005(20)
end
@divisors (20..2) |> Enum.to_list
defp problem005(number) do
if Enum.all?(@divisors, &(rem(number, &1) == 0)) do
number
else
problem005(number+20)
end
end
end
Il me faut environ 1,4 seconde sur mon ordinateur portable. Le problème principal de votre solution est la conversion d’une plage en liste à chaque itération. C'est un énorme frais généraux. En outre, il n'est pas nécessaire de créer un flux "infini" ici. Vous n'avez pas fait quelque chose comme ça dans d'autres langues.
Votre code est correct, mais le calcul me fait mal aux dents. Il existe une solution récursive simple qui correspond bien à la manière de faire les choses avec élixir. Elle montre également comment faire de la récursivité avec élixir sans se soucier des problèmes de performances causés par la récursion dans d’autres. langues.
defmodule Euler_5 do
@moduledoc """
Solve the smallest number divisible by 1..X using Greatest Common Divisor.
"""
def smallest(1), do: 1
def smallest(2), do: 2
def smallest(n) when n > 2 do
next = smallest(n-1)
case rem(next, n) do
0 -> next
_ -> next * div(n,gcd(next,n))
end
end
def gcd(1,_n), do: 1
def gcd(2,n) do
case rem(n,2) do
0 -> 2
_ -> 1
end
end
def gcd( m, n) do
mod = rem(m,n)
case mod do
0 -> n
_ -> gcd(n,mod)
end
end
end
Pour ce que ça vaut, cela prend 8 microsecs sur mon ordinateur
iex> :timer.tc(Euler_5, :smallest, [20])
{8, 232792560}
Ce n’est pas vraiment une comparaison juste avec d’autres langues car cela ne prend pas le temps de charger la VM et de faire les E/S.
J'aime cette solution pour sa simplicité:
#!/usr/bin/env elixir
defmodule Problem005 do
defp gcd(x, 0), do: x
defp gcd(x, y), do: gcd(y, rem(x, y))
defp lcm(x, y) do
x * y / gcd(x, y)
end
def solve do
1..20
|> Enum.reduce(fn(x, acc) -> round(lcm(x, acc)) end)
end
end
IO.puts Problem005.solve
C'est très rapide aussi.
./problem005.exs 0.34s user 0.17s system 101% cpu 0.504 total
Quant à Ruby , cela peut être résolu en une seule ligne:
#!/usr/bin/env Ruby
puts (1..20).reduce { |acc, x| acc.lcm(x) }
(lcm -> http://Ruby-doc.org/core-2.0.0/Integer.html#method-i-lcm )
La solution de Fred est géniale. C'est plus inefficace (32 microsecondes) mais plus clair. Peut-être qu'avec la mémorisation, cela pourrait être beaucoup plus rapide.
defmodule Euler5 do
def smallest(n) when n > 0 do
Enum.reduce(1..n, &(lcm(&1, &2)))
end
def smallest(n), do: n
def lcm(x, y), do: div((x * y), gcd(x, y))
def gcd(x, 0), do: x
def gcd(x, y), do: gcd(y, rem(x, y))
end