如何在 Erlang 或 Elixir 中生成 n-k 轻量级进程?

3

在Go语言中,我可以像这样创建goroutine(根据kelu-thatsall的回答进行了编辑):

// test.go
package main

import (
        "fmt"
        "os"
        "strconv"
        "sync"
        "runtime"
)

func main() {
        var wg sync.WaitGroup
        if len(os.Args) < 2 {
                os.Exit(1)
        }
        k, ok := strconv.Atoi(os.Args[1])
        if ok != nil {
                os.Exit(2)
        }
        wg.Add(k * 1000)
        for z := 0; z < k*1000; z++ {
            go func(x int) {
                    defer wg.Done()
                    fmt.Println(x)
            }(z)
            if z%k == k-1 {
                // @mattn: avoid busy loop, so Go can start processing like BEAM do 
                runtime.Gosched() 
            }
        }
        wg.Wait()
}

在 Go 1.8.0 (64 位) 中的结果:
# shell    
$ go build test.go ; for k in 5 50 500 5000 50000 500000; do echo -n $k; time ./test $k > /dev/null; done

5
CPU: 0.00s      Real: 0.00s     RAM: 2080KB
50
CPU: 0.06s      Real: 0.01s     RAM: 3048KB
500
CPU: 0.61s      Real: 0.12s     RAM: 7760KB
5000
CPU: 6.02s      Real: 1.23s     RAM: 17712KB # 17 MB
50000
CPU: 62.30s     Real: 12.53s    RAM: 207720KB # 207 MB
500000
CPU: 649.47s    Real: 131.53s   RAM: 3008180KB # 3 GB

在Erlang或者Elixir中,对应的代码是什么?(根据patrick-oscity的评论进行了编辑

目前我尝试过的是以下内容:

# test.exs
defmodule Recursion do
  def print_multiple_times(n) when n <= 1 do
    spawn fn -> IO.puts n end
  end

  def print_multiple_times(n) do
    spawn fn -> IO.puts n end
    print_multiple_times(n - 1)
  end
end

[x]=System.argv()
{k,_}=Integer.parse(x)
k=k*1000
Recursion.print_multiple_times(k)

在 Elixir 1.4.2(erts-8.2.2)中的结果是:
# shell    
$ for k in 5 50 500 5000 50000 ; do echo -n $k; time elixir --erl "+P 90000000" test.exs $k > /dev/null; done

5
CPU: 0.53s      Real: 0.50s     RAM: 842384KB # 842 MB
50
CPU: 1.50s      Real: 0.62s     RAM: 934276KB # 934 MB
500
CPU: 11.92s     Real: 2.53s     RAM: 1675872KB # 1.6 GB
5000
CPU: 122.65s    Real: 20.20s    RAM: 4336116KB # 4.3 GB
50000
CPU: 1288.65s   Real: 209.66s   RAM: 6573560KB # 6.5 GB

但我不确定这两者是否等价。它们是吗?
编辑 缩短版本,因为mudasobwa的评论没有给出正确的输出。
# test2.exs
[x]=System.argv()
{k,_}=Integer.parse(x)
k=k*1000
1..k |> Enum.each(fn n -> spawn fn -> IO.puts n end end)

执行结果 for k in 5 50 500 5000 50000 ; do echo -n $k; time elixir --erl "+P 90000000" test.exs $k | wc -l ; done:

5
CPU: 0.35s      Real: 0.41s     RAM: 1623344KB # 1.6 GB
2826 # does not complete, this should be 5000
50
CPU: 1.08s      Real: 0.53s     RAM: 1691060KB # 1.6 GB
35062
500
CPU: 8.69s      Real: 1.70s     RAM: 2340200KB # 2.3 GB
373193
5000        
CPU: 109.95s    Real: 18.49s    RAM: 4980500KB # 4.9 GB
4487475
50000
erl_child_setup closed
Crash dump is being written to: erl_crash.dump...Command terminated by signal 9
CPU: 891.35s    Real: 157.52s   RAM: 24361288KB # 24.3 GB

因为测试500m需要太长时间,而且+P 500000000参数是进程数错误,所以不会对elixir进行测试。

微基准测试结果


1
也许可以解释一下为什么你认为它们不等价? - Sridhar
1
你是在尝试比较Go和Elixir中生成进程的性能吗?只是一个猜测,但IO可能会成为你的瓶颈。此外,在你的Elixir代码中有一个偏移错误,你实际上生成了n+1个进程 - 尽管这在这里不会有太大的区别。为了获得有意义的基准测试,了解你实际想要在进程中执行什么类型的工作将是有趣的。 - Patrick Oscity
2
在我看来,它们几乎是等价的。如果我将代码放入混合包中编译,然后运行,我可以获得约15-20%的加速。即使我完全从Elixir中删除IO.puts,它仍然比当前的代码快30%,但比你发布的Go数字要慢得多。 - Dogbert
@mudasobwa,你的版本在尾递归方面有何不同? - Hynek -Pichi- Vychodil
1..k |> Enum.each(fn n -> spawn fn -> IO.puts n end end)版本不能正确输出行号,就像第一版Go一样。 - Kokizzu
显示剩余3条评论
1个回答

3
抱歉大家,但我不确定这段Go代码是否真的按预期工作。如果我错了,请纠正我,因为我不是专家。首先,它会打印出 z,它似乎是全局范围内的当前值(通常为 k*1000)。https://play.golang.org/p/a4TJyjKBQh
// test.go
package main
import (
  "fmt"
  "time"
)

func main() {

  for z:=0; z<1000; z++ {
    go func(x int) { // I'm passing z to the function with current value now
      fmt.Println(x) 
    }(z)
  }

  time.Sleep(1 * time.Nanosecond)

}

如果我注释掉 Sleep,那么程序在启动任何goroutine之前就会退出(至少不会打印出结果)。我很乐意知道我是否做错了什么,但从这个简单的例子中看来,问题并不是出在 Elixir 上,而是出在提供的 Go 代码上。有一些 Go 大师在吗?

我也在本地机器上运行了一些测试:

go run test.go 500 | wc -l
72442 # expected 500000
go run test.go 5000 | wc -l
76274 # expected 5000000

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接