为什么 Elixir 在 Ruby 和 Go 中解决 Project Euler #5 最慢?
Why is Elixir slowest among Ruby and Go in solving Project Euler #5?
更新:Elixir 并不慢,我的算法是。我的算法甚至不是同类比较。有关 Ruby 和 Go 等效算法的信息,请参见下面 Roman 的回答。还要感谢 José,只需添加 MIX_ENV=prod 前缀就可以显着加快我的慢速算法。我已经更新了问题中的统计数据。
原题:
我正在用多种语言处理 Project Euler 问题,只是为了看看语言的效率和速度。在problem #5中,我们被要求找到能被1到20的所有数字整除的最小正数。
我用多种语言实现了解决方案。以下是统计数据:
- Go 1.4.2:0.58 秒
- Ruby 2.2 MRI : 6.7s
- Elixir 1.0.5(我的第一个算法):57s
- Elixir 1.0.5(我的第一个带有 MIX_ENV=prod 前缀的算法):7.4s
- Elixir 1.0.5(罗马的围棋等效算法):0.7s
- Elixir 1.0.5(罗马的 Ruby 等效算法):1.8s
为什么 Elixir 的性能这么慢?我尝试在所有语言中使用相同的优化。警告:我是 FP 和 Elixir 新手。
我可以做些什么来提高 Elixir 的性能吗?如果您使用任何分析工具来找出更好的解决方案,请将它们包含在响应中吗?
围棋:
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
在长生不老药中:
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
试试这个版本:
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 秒。
您的解决方案的主要问题是在每次迭代时将范围转换为列表。这是一个巨大的开销。此外,这里不需要创建 "infinite" 流。你没有用其他语言做类似的事情。
我的第一个答案是关于实现您在 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 在这里也可以发挥得更好。
我想,这里的一般规则是:如果你在 Elixir 中的代码具有 Go 代码的 80% 或更好的性能,那没关系。在其他情况下,您的 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 秒。
你的代码可能没问题,但数学让我牙疼。有一个简单的递归解决方案可以很好地匹配长生不老药的做事方式。
它还展示了如何在 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 的时间。
Fred 的解决方案很棒。这效率较低(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
我喜欢这个简单的解决方案:
#!/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,这个可以一行解决:
#!/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)
更新:Elixir 并不慢,我的算法是。我的算法甚至不是同类比较。有关 Ruby 和 Go 等效算法的信息,请参见下面 Roman 的回答。还要感谢 José,只需添加 MIX_ENV=prod 前缀就可以显着加快我的慢速算法。我已经更新了问题中的统计数据。
原题: 我正在用多种语言处理 Project Euler 问题,只是为了看看语言的效率和速度。在problem #5中,我们被要求找到能被1到20的所有数字整除的最小正数。
我用多种语言实现了解决方案。以下是统计数据:
- Go 1.4.2:0.58 秒
- Ruby 2.2 MRI : 6.7s
- Elixir 1.0.5(我的第一个算法):57s
- Elixir 1.0.5(我的第一个带有 MIX_ENV=prod 前缀的算法):7.4s
- Elixir 1.0.5(罗马的围棋等效算法):0.7s
- Elixir 1.0.5(罗马的 Ruby 等效算法):1.8s
为什么 Elixir 的性能这么慢?我尝试在所有语言中使用相同的优化。警告:我是 FP 和 Elixir 新手。
我可以做些什么来提高 Elixir 的性能吗?如果您使用任何分析工具来找出更好的解决方案,请将它们包含在响应中吗?
围棋:
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
在长生不老药中:
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
试试这个版本:
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 秒。 您的解决方案的主要问题是在每次迭代时将范围转换为列表。这是一个巨大的开销。此外,这里不需要创建 "infinite" 流。你没有用其他语言做类似的事情。
我的第一个答案是关于实现您在 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 在这里也可以发挥得更好。
我想,这里的一般规则是:如果你在 Elixir 中的代码具有 Go 代码的 80% 或更好的性能,那没关系。在其他情况下,您的 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 秒。
你的代码可能没问题,但数学让我牙疼。有一个简单的递归解决方案可以很好地匹配长生不老药的做事方式。 它还展示了如何在 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 的时间。
Fred 的解决方案很棒。这效率较低(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
我喜欢这个简单的解决方案:
#!/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,这个可以一行解决:
#!/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)