为什么Elixir的Enum.any?慢?

时间:2015-10-01 02:11:40

标签: profiling elixir

我跑到命令

下面
MIX_ENV=prod  mix profile.fprof --no-start -e "Math.prime_seq 501"

以下代码

  def prime_seq(n) do
    prime_seq(n, 1, 3, [2,3,5,7,11,13,17,19,23])
  end

  def prime_seq(n, c, p, cache) when c < n do
    is_it = cache |> Enum.any?(fn n -> rem(p, n) == 0 end)
    if not is_it do
      prime_seq(n, c+1, p+2, cache ++ [p])
    else
      if(is_prime(p)) do
        prime_seq(n, c+1, p+2, cache ++ [p])
      else
        prime_seq(n, c, p+2, cache)
      end
    end
  end

  def prime_seq(n, c, p, _) when c == n do
    p-2
  end

结果:

profile result

为什么Enum.do_any?需要花费太多时间?

是的,这是一个愚蠢的算法来查找第n个素数,并且有更好的算法。但重点是,使Enum.any?比使用专门的函数迭代列表要慢。

我相信anom func是rem(p,n),CMIIW

更新 我删除了Enum.any?,名为divisible?

 def divisible?(n, [h|t]) do
    if rem(n, h) == 0 do
      true
    else
      divisible?(n, t)
    end
  end

  def divisible?(n, []) do
    false
  end
  ..... 
  def prime_seq(n, c, p, cache) when c < n do
    #is_it = cache |> Enum.any?(fn n -> rem(p, n) == 0 end)
    is_it = divisible?(p, cache)
    if not is_it do
      prime_seq(n, c+1, p+2, cache ++ [p])
    else
      if(is_prime(p)) do
        prime_seq(n, c+1, p+2, cache ++ [p])
      else
        prime_seq(n, c, p+2, cache)
      end
    end
  end
  .....

结果:

faster

所以..通过简单的修改,我可以使它快3倍,迭代次数也相同。

注意:在学习灵丹妙药时,这是一个玩具项目。请耐心等待。

is_prime函数:

def is_prime(n, i) when i < n do
    if rem(n, i) == 0 do
      false
    else
      is_prime(n, i+1)
    end
  end
  def is_prime(n, i) when i >= n do
    true
  end
  def is_prime(n) do
    cond do
    n <= 1 -> false
    n <= 3 -> true
    rem(n, 2) == 0 or rem(n, 3) == 0 -> false
    true -> is_prime(n, 3)
    end
  end

5 个答案:

答案 0 :(得分:5)

因为我被问过,所以我会把它作为答案而不是评论。

问题不在于匿名函数是“昂贵的”,而是代码几乎没有做任何事情,只是在列表上进行迭代。

BEAM调度程序使用缩减来执行功能切片。这有点复杂,但每个函数调用都算作一次减少。当您使用匿名函数时,您正在增加缩减的时间成本(即,将实际函数的查找添加到时间成本中 减少。)通常这个额外的成本可以忽略不计,但是当你这样做时 数百万次,它加起来。

BEAM调度程序为每个进程2000减少,然后为时间片 一个新的过程。

您创建了一个病态边缘案例,将零值与另一个值进行比较。如果您将零与任何看起来“昂贵”的东西进行比较,那么无论绝对比例的值有多大或多小都无关紧要。

正确的结论是,比O(n)缩放得更快的递归算法对每次递归中完成的工作量非常敏感。你应该感到惊讶的是,这根本不起作用,而不是它很慢。

如果我今天有时间,我会尝试使用以下各种情况获得一些减少计数:erlang.statistics(:exact_reductions)。

我使用此代码来获取一些基本指标。

defmodule Counter do

 def count(function,arg) do
  {_ , count } = :erlang.process_info(self,:reductions)
  function.(arg)
  {_ , new_count } = :erlang.process_info(self,:reductions)
  new_count - count
 end

end

这个代码计算减少的方式并不完美,它应该真的在它的运行 自己的过程。

这是我得到的结果,首先是Enum.any?版本

iex(4)> Counter.count(&Math.prime_seq/1, 10)
283
iex(5)> Counter.count(&Math.prime_seq/1, 100)
14086
iex(6)> Counter.count(&Math.prime_seq/1, 1000)
1105114
iex(7)> Counter.count(&Math.prime_seq/1, 10000)
103654258
iex(8)> Counter.count(&Math.prime_seq/1, 100000)
10068833898

请注意,所有这些减少都发生在一个调度线程中,我的8核笔记本电脑在此测试期间几乎没有忙。很明显,这是一种减少O(n ** 2)算法。现在有了可分割的功能

iex(1)> Counter.count(&Math.prime_seq_div/1, 10)
283
iex(2)> Counter.count(&Math.prime_seq_div/1, 100)
14062
iex(3)> Counter.count(&Math.prime_seq_div/1, 1000)
1105485
iex(4)> Counter.count(&Math.prime_seq_div/1, 10000)
103655170
iex(5)> Counter.count(&Math.prime_seq_div/1, 100000)
10068870615

老实说,我预计这些数字会更小。事实上,他们并没有让我得出关于正在发生的事情的另一个结论,这不是减少,而是减少更多的时间。

答案 1 :(得分:1)

查看source code of Enum.do_any?,我们可以看到它所做的就是遍历列表并调用提供的lambda。

分析结果似乎表明大部分时间都花在lambda之外,即在迭代时,这在某种程度上让我困惑。

无论如何,对这些结果的解释是大部分时间花在这一行上:

is_it = cache |> Enum.any?(fn n -> rem(p, n) == 0 end)

另一个有用的信息是代码对输入大小为501进行了135k次迭代。这是一个非常好的指示,算法复杂度至少是多项式。

基于此,我建议考虑一些算法改变,例如Eratosthenes的筛子。不幸的是,我无法弄清楚这个代码假设要返回什么,所以我无法提供替代解决方案。

答案 2 :(得分:0)

如果您需要极端性能,Elixir / Erlang可能不是正确的工具。 Erlang VM性能不适合CPU绑定任务。相反,您可能需要调用高度优化的外部库,例如primesieve。您可以学习如何编写所谓的端口,以便在this recent Elixir Sips episode(免费)中调用本机代码。

答案 3 :(得分:0)

要回答为什么divisible?慢于cache |> Enum.any?(fn n -> rem(p, n) == 0 end)的问题我为非分析版本创建了一个简单的测试用例:

  1. divisible?(p, cache)
  2. divisible_anon?(cache, fn n -> rem(p, n) == 0 end)
  3. def divisible_anon?([h|t], fun) do if fun.(h) do true else divisible_anon?(t, fun) end end def divisible_anon?([], _) do false end def divisible?(n, [h|t]) do if rem(n, h) == 0 do true else divisible?(n, t) end end def divisible?(n, []) do false end
  4. 测试代码是:

      def measure(function) do
        function
        |> :timer.tc
        |> elem(0)
        |> Kernel./(1_000_000)
      end
    

    测量:

    Math.prime_seq 501

    Math.prime_seq 10001的结果:

    1. 0.009852 s
    2. 0.008797 s
    3. 0.012836 s
    4. {{1}}的结果:

      1. 2.686799 s
      2. 1.395422 s
      3. 2.687937 s
      4. 结论,在@sasajuric的帮助下:&#34;匿名函数调用很昂贵&#34;

答案 4 :(得分:0)

Enum.any?是通过依赖Enumerable(使用reducees)来实现的:http://blog.plataformatec.com.br/2015/05/introducing-reducees/

泛化与专业化相反。因此,虽然我们可以使用任何数据类型,但它不会像您自己实现列表变体一样快。但是,这并不意味着我们必然会变慢,我们可以轻松地在Enum中内联这些操作,从而降低成本。