【问题标题】:Why is Elixir slowest among Ruby and Go in solving Project Euler #5?为什么 Elixir 在 Ruby 和 Go 中解决 Project Euler #5 最慢?
【发布时间】:2015-09-07 12:38:16
【问题描述】:

更新:Elixir 并不慢,我的算法很慢。我的算法甚至不是苹果对苹果的比较。有关 Ruby 和 Go 等效算法的信息,请参阅下面的 Roman 的答案。还要感谢 José,只需添加 MIX_ENV=prod 前缀就可以显着加快我的慢速算法。我已经更新了问题中的统计信息。

原问题: 我正在研究多种语言的 Project Euler 问题,只是为了看看语言的效率和速度。在problem #5 中,我们被要求找到能被 1 到 20 的所有数字整除的最小正数。

我以多种语言实现了该解决方案。以下是统计数据:

  1. 去 1.4.2 : 0.58s
  2. Ruby 2.2 MRI:6.7 秒
  3. Elixir 1.0.5(我的第一个算法):57s
  4. Elixir 1.0.5(我的第一个带有 MIX_ENV=prod 前缀的算法):7.4s
  5. Elixir 1.0.5(Roman's Go 等效算法):0.7s
  6. Elixir 1.0.5(Roman 的 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

【问题讨论】:

  • 我无法解释你的 Elixir,但几乎可以肯定有更好的方法来解决这个问题,例如尝试构建数字,而不是仅仅扫描它。
  • 谢谢,鲁普。我会试试这个建议。但是,我在这里的问题纯粹与类似逻辑的性能有关,是 3 种不同的语言。
  • 如果您对 Elixir 代码进行基准测试,请确保使用 MIX_ENV=prod 对其进行基准测试,以便 Elixir 尽可能高效地编译您的项目。否则,您将获得次优性能。

标签: performance elixir numerical-computing


【解决方案1】:

我的第一个答案是关于实现您在 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 中的代码具有 80% 的 Go 代码或更好的性能,那没关系。在其他情况下,您的 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 秒。

【讨论】:

  • 再次感谢您的回答!这在我的系统上大约需要 0.7 秒。
  • 太棒了!很高兴为您提供帮助:-)
【解决方案2】:

试试这个版本:

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 秒。 您的解决方案的主要问题是在每次迭代时将范围转换为列表。这是一个巨大的开销。此外,这里不需要创建“无限”流。你没有用其他语言做类似的事情。

【讨论】:

  • 谢谢,罗曼!你的算法在我的系统上大约需要 1.8 秒。最大的罪魁祸首确实是范围到列表的转换。从范围创建列表一次将运行时间从 57 秒缩短到 2.7 秒。
  • 值得注意的是,MIX_ENV=prod 将大大加快范围到列表的转换(因为我们内联了这些),并且 Elixir 1.1 中的范围也会更快。
  • 谢谢,何塞!通过在我令人尴尬的缓慢算法中添加前缀 MIX_ENV=prod,我确实看到了显着的改进(57.0s 到 7.4s)。我已经更新了问题中的统计数据。
【解决方案3】:

你的代码可能没问题,但数学让我牙疼。有一个简单的递归解决方案可以很好地匹配长生不老药的做事方式。 它还展示了如何在 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 的时间。

【讨论】:

  • 谢谢,弗雷德。 8微秒令人印象深刻!我试图不偷看你的解决方案,所以我可以自己尝试 GCD 方法。在我完成自己的解决方案后,我将包含您解决方案的统计数据。
【解决方案4】:

我喜欢这种简单的解决方案:

#!/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)

【讨论】:

  • 和 ruby​​ 一起走得更远...(1..20).inject(&:lcm)
【解决方案5】:

Fred 的解决方案很棒。这更无效率(32 微秒)但更清晰。也许通过 meomization,它的运行速度可以提高一个数量级。

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

【讨论】:

    猜你喜欢
    • 2013-08-11
    • 2023-02-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-07-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多