【发布时间】:2015-04-19 16:02:58
【问题描述】:
我正在学习 Elixir 作为我的第一个函数式语言。作为第一个熟悉环境和语法的简单项目,我选择构建一个简单的程序来计算命令行上提供的数字的质因数。这是我的第一个解决方案:
defmodule Prime do
defp is_factor?(number, divisor) do
cond do
rem(number, divisor) == 0 -> divisor
true -> nil
end
end
defp not_nil?(thing) do
!is_nil(thing)
end
def factors(number) when number == 1 do
[]
end
def factors(number) do
1..div(number, 2)
|> Enum.map(&(is_factor?(number, &1)))
|> Enum.filter(¬_nil?/1)
end
def is_prime?(number) when number == 1 do
true
end
def is_prime?(number) do
factors(number) == [1]
end
def prime_factors(number) do
factors(number)
|> Enum.filter(&is_prime?/1)
end
end
input = hd(System.argv)
number = String.strip(input) |> String.to_integer
IO.puts "Prime factors of #{number} are #{inspect Prime.prime_factors(number)}"
它可以工作,但运行速度很慢。在我的笔记本电脑上,计算 50,000,000 的质因数大约需要 11 秒。
随着我阅读的更多,这个原始解决方案似乎不是很像 Elixir。所以我将代码重组为:
defmodule PrimeFactors do
def of(n) do
_factors(n, div(n, 2))
end
defp _factors(_n, 1) do
[1]
end
defp _factors(n, divisor) when rem(n, divisor) == 0 do
cond do
is_prime?(divisor) -> _factors(n, divisor - 1) ++ [divisor]
true -> _factors(n, divisor - 1)
end
end
defp _factors(n, divisor) do
_factors(n, divisor - 1)
end
defp is_prime?(1) do
true
end
defp is_prime?(n) do
of(n) == [1]
end
end
input = hd(System.argv)
number = String.strip(input) |> String.to_integer
IO.puts "Prime factors of #{number} are #{inspect PrimeFactors.of(number)}"
此代码计算 50,000,000 的素因数的典型运行时间要差得多:超过 17 秒。
我用 Swift 和 Ruby 构建了等效的程序。优化后的 Swift 运行只需 0.5 秒多一点,而 Ruby(2.2,从未以速度着称)运行只需 6 秒多一点。
我的主要问题是:应该如何将 Elixir 代码的结构安排得更符合习惯并避免我看到的性能问题?
我还有些担心,考虑到这样一个简单的问题,编写效率差异很大的 Elixir 代码是可能的。也许这主要是我在功能样式展示方面的经验不足?
【问题讨论】:
-
当您想将元素添加到列表时,您可以肯定做的一件事是避免使用
++运算符。 Elixir 列表由cons cells 组成,因此将元素添加到列表末尾应该是 O(n) 操作,而将其添加到列表头部是 O(1)。一种常见的模式是将元素添加到列表 ([new_el|list]) 并在使用之前使用Enum.reverse/1反转列表。 -
很高兴知道,但在这种情况下并没有太大的区别。我猜这是因为素数列表往往很短(对于 50,000,000,列表是 [1, 2, 5])。
-
好的,另一个很好的建议,但算法在其他实现中是相同的。我觉得我缺少对如何构造 Elixir 代码以避免性能问题的基本理解。看起来 Ruby 实现和好的 Elixir 实现之间不应该有这么大的差距。
-
该代码对于大数字肯定很慢,但我相信其主要原因是算法复杂性。这是我对解决方案的快速和草率的看法,该解决方案很可能远非最佳,但比原始代码运行得更快:gist.github.com/sasa1977/98e6465bf854a9d86cba
-
原始速度不是 Erlang 的主要关注点,所以如果它对 CPU 的限制很大,我预计 Elixir/Erlang 会比大多数主流语言慢。在这个示例中,我怀疑 Ruby 会胜出,因为您使用 C 实现的 Ruby 函数(map、reject、compact)进行了大量迭代,而 Elixir 迭代是标准的 Erlang 字节码。此外,(在此示例中不相关),使用
time测量可能影响结果的 Erlang VM 启动时间。我真的不认为这是一个很好的例子,因为一旦你在算法上优化代码,差异可能变得微不足道。
标签: elixir