Update:Elixirは遅くありません、私のアルゴリズムは遅くありませんでした。私のアルゴリズムは、アップルトゥアップルの比較でさえありませんでした。 RubyおよびGoと同等のアルゴリズムについては、以下のRomanの回答を参照してください。また、Joséのおかげで、MIX_ENV = prodのプレフィックスを付けるだけで、遅いアルゴリズムを大幅に高速化できます。質問の統計を更新しました。
元の質問:私は、言語の生産性と速度を確認するために、複数の言語でプロジェクトオイラーの問題に取り組んでいます。 問題#5 では、1から20までのすべての数で均等に割り切れる最小の正の数を見つけるように求められます。
ソリューションを複数の言語で実装しました。統計は次のとおりです。
Elixirのパフォーマンスが非常に遅いのはなぜですか?すべての言語で同じ最適化を使用してみました。警告:私はFPそしてElixirの初心者です。
Elixirのパフォーマンスを向上させるためにできることはありますか?より良い解決策を見つけるためにプロファイリングツールを使用した場合、それらを応答に含めていただけますか?
Goで:
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")
}
Rubyの場合:
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
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
私の最初の答えは、Rubyで実装したのと同じアルゴリズムを実装することについてでした。さて、これがGoのアルゴリズムのElixirのバージョンです。
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
私のラップトップでは約0.73秒かかります。これらのアルゴリズムは異なるので、Rubyもここでうまく機能する可能性があると確信しています。
ここでの一般的なルールは、Goコードから80%以上のパフォーマンスを持つElixirのコードがある場合は問題ありません。その他の場合、Elixirコードにアルゴリズムエラーがある可能性があります。
Rubyに関する更新:
ボーナスとして、RubyのGoと同等のアルゴリズムは次のとおりです。
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
それは4.5倍速く実行するので、私はそれがあなたのコンピュータ上で約1.5秒を示すことができると思います。
このバージョンを試してください:
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
私のラップトップでは約1.4秒かかります。ソリューションの主な問題は、反復ごとに範囲をリストに変換することです。それは大きなオーバーヘッドです。また、ここで「無限」のストリームを作成する必要はありません。あなたは他の言語でそのようなことをしませんでした。
あなたのコードは問題ないかもしれませんが、数学は私の歯を傷つけます。物事を行う秘薬の方法にうまく一致する単純な再帰的解決策があります。また、他の言語で再帰が引き起こすパフォーマンスの問題を心配せずに、elixirで再帰を実行する方法も示しています。
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
それが価値があるもののために、これは私のコンピュータで8マイクロ秒かかります
iex> :timer.tc(Euler_5, :smallest, [20])
{8, 232792560}
VMをロードしてI/Oを実行する時間が含まれていないため、他の言語との比較は実際には公平ではありません。
私はその単純さのためにこのソリューションが好きです:
#!/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
非常に高速でもあります。
./problem005.exs 0.34s user 0.17s system 101% cpu 0.504 total
Rubyに関しては、これは1行で解決できます。
#!/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 )
フレッドの解決策は素晴らしいです。これはより非効率的(32マイクロ秒)ですが、より明確です。たぶんメモ化で、それは桁違いに速く走ることができます。
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