web-dev-qa-db-fra.com

Pourquoi Elixir est-il le plus lent parmi Ruby et Go dans la résolution du projet Euler # 5?

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:

  1. Go 1.4.2: 0.58s
  2. Ruby 2.2 MRI: 6.7s
  3. Elixir 1.0.5 (mon premier algorithme): 57s
  4. Elixir 1.0.5 (mon premier algorithme avec MIX_ENV = préfixe prod): 7.4s
  5. Elixir 1.0.5 (algorithme équivalent de Roman's Go): 0.7s
  6. Elixir 1.0.5 (algorithme équivalent de Roman à Ruby): 1.8s

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
17
Abhishek S

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.

11
Roman Smirnov

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.

5
Roman Smirnov

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 )

2
Frank Kair

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
1
AA.