【问题标题】:Better way writing nested for loops in Elixir在 Elixir 中编写嵌套 for 循环的更好方法
【发布时间】:2019-05-19 03:32:15
【问题描述】:

请提出更好的方法(更多的elixir方式)将以下C代码写入elixir。

int some_num = 0;

for(int i = 0; i < 100; i++){
  for(int j = 0; j < 1000; j++){
    for(int k = 0; k < 10000; k++){
      some_num += 1;
    }
  }
}
printf("%d", some_num);

是否可以通过获得好处elixir并发来实现?

编辑: 一点背景知识,我对长生不老药很新鲜,还在学习。这个问题的主要动机是编写更多惯用的灵丹妙药代码而不是应用并发。

【问题讨论】:

  • increment 是一个可以单独完成的操作——所以它不是 Elixir 用于的典型任务。但是您可以将每个内部循环划分为 4 个单独的 erlang/elixir 进程,并在每个进程完成时通过接收来自进程的消息来收集它们的结果。我说因为我的电脑有 4 个核心。 ymmv。
  • Elixir 中最先进的并发方法是采用 Flow 模块github.com/antonmi/flowex,但调整您的示例以使用它意味着首先将代码更改为更多 Elixir 惯用语。
  • @GavinBrelstaff:你指向了错误的Flow 库。
  • 如果你能指出正确的 - 我找不到如何在不调用 mix 等的情况下使用 Flow 模块。

标签: elixir


【解决方案1】:

准确实现您所写内容的最简单方法是使用for 宏:

sum =
  for i <- 0..100,
      j <- 0..1_000,
      k <- 0..10_000,
      reduce: 0 do
    acc -> acc + 1
  end

编辑:

:reduce 选项在 Elixir (1.8+) 的新版本中可用。在旧版本中,您可以使用嵌套的Enum.reduce/3

Enum.reduce(0..100, 0, fn _, acc ->
  acc + Enum.reduce(0..1_000, 0, fn _, acc ->
    acc + Enum.reduce(0..10_000, 0, fn _, acc ->
      acc + 1
    end)
  end)
end)

关于问题的第二部分:,这个循环不会从并发中获得太多收益,如果它会以任何方式改变时间,那么它只会变慢。在这种特殊情况下,它可以写成sum = 100 * 1_000 * 10_000,这样可以更快,因为编译器可以很容易地将其优化为10_000_000(IIRC Erlang 编译器无法将给定循环优化为常量)。

TL;DR 这种明显的循环不能通过并发来改善,一般情况下很难说进程(又名并行化)是否有帮助。记住parallel != concurrent 也很重要,因此在使用N-1 调度程序(默认CPU 数量)的机器上运行N Erlang 的进程不会获得任何加速。

【讨论】:

  • for 宏从何而来? Elixir shell 为您发布的代码提供(CompileError) iex:3: unsupported option :reduce given to for
  • Kernel.SpecialForms.for/1,可能你有旧版本的不支持:reduce选项的Elixir。
  • 嗨@Hauleth 在现实世界(我的 4 核 Win10 PC)中,您的代码甚至需要很长时间才能返回提示 - 而我给出的代码在 16 秒后终止。
  • 因为 Erlang 优化还不是那么好。理论上(以及在实践中的某些编译器中)它将是 optimized out 到单个常量。仍然在这个玩具代码中它可以获得一些东西,但最终 Elixir 想要直接编译为 BEAM IR,然后可以优化这些代码,通过引入进程你将阻止这种优化。正如前面所说,它可以用更好的方式来写,所以整个问题几乎没有意义。
  • +1 用于提及新的 :reduce 选项。带有箭头的奇怪语法——就像你在for 中有一个单独的分支case 语句。文档中有nice examplesoptions
【解决方案2】:

下面是一个工作示例,说明如何为实现 1000000000 次增量操作的玩具问题实现并发 - 如果您对如何完成感到好奇。

下面的代码生成 100 个 Elixir 进程,对应于您的外部循环。内部代码——这两个嵌套循环——(使用Enum.reducesee以更惯用的形式编写)因此同时运行(尽可能由VM)。每个进程的结果被发送到一个专用的接收进程,每当它收到一个新的结果时,它就会从 100 开始倒计时。每个小计都会添加到总计中,然后在收到 100 个小计时打印出来。 测试:将代码保存为文件nested.ex 并使用c nested.ex 在Elixir shell 中编译。使用 Main.main 在该 shell 中运行它。您应该会看到以下输出:

iex(4)> Main.main    
:ok
total = 1000000000

oktotal 之前几秒钟出现。您还应该体验高 cpu 多核使用率。

defmodule Main do 
def main(  ) do
    pid = spawn fn -> Receiver.loop( 100,0 ) end
    1..100 |> Enum.each( fn x -> spawn (fn -> Nested.run(pid) end ) end)
  end

end

#################################
defmodule Nested do
 def run(pid) do
    sub_total= 
    Enum.reduce( 1..1000, 0, fn x, acc_n -> acc_n +
      Enum.reduce( 1..10000, 0, fn y, acc_m -> acc_m + 1  end )
    end )
   send pid, sub_total
   Process.exit(self(), :kill )
 end
end

#################################
defmodule Receiver do
 def loop(0, total) do
   IO.puts "total = #{total}"
   Process.exit(self(), :kill )
 end
 #
 def loop(count_down, total ) do # count down to zero collecting totals
    receive do
      sub_total ->
        loop(count_down-1, sub_total + total)
    end
 end
end
#################################

通过明智地将普通spawn转换为Node.spawnsee docs

,可以获得并行性以获得优势而不是纯并发

非正式速度测试 在我的 Win10 电脑上测试报告:

Erlang/OTP 20 [erts-9.0] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10]
Interactive Elixir (1.8.2) ..

我在这里给出的代码在 16 秒内计算出结果,而 @Hauleth 的结果需要 10 多分钟——因为他似乎只分配了一个核心,而我的核心得到了全部 4 个。

【讨论】:

  • 为什么产生了这么多进程?
  • @Hauleth 这就是 Elixir/Erlang 的方式 - spawn 很多小进程,让 VM 整理并发优化。 100并不多。另见poeticoding.com/…
  • 我知道 Erlang 并且可以肯定这既不是 Erlang 方式也不是任何类型的优化。如果有的话,它只会导致减速。 “Erlang方式”是写sum = 100 * 1_000 * 10_000
  • Hauleth GavinBrelstaff 我是 elixir 的新手,我只是好奇如何在 elixir 中编写更多惯用的嵌套循环,我们可以从 elixir 的并发性中获得什么好处。
  • 不幸的是,这些微不足道的问题几乎永远无法为您提供真实世界的答案。这只是显示了您可以尝试在应用程序中实现并发的一种方法。这可能不是您可能遇到的所有问题的最佳解决方案。如果您考虑到真实世界的场景,最好询问一下。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-01-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多