【追記】
tatsuya6502 さん に、コメント頂いた内容を踏まえ、追記・修正を行いました。また、ついでに、ちょっと気になったのでElixirのRangeとErlangの:lists.seqの速度比較もやってみました。
Elixir(= Erlang)の特長の一つに、子プロセスを生成して並列実行することで、各 CPU core を活用することができるという事があります。
各言語で0〜50,000,000までの数字を加算するロジックを書き、ベンチマークと htop による CPU 利用率を測定してみました。
環境
- さくらVPSの4G (CPU 4 core, RAM:4GB)
- C言語: gcc 4.4.7
- elixir: 1.1.1(Erlang 18.2.1)
- python: 2.7.8
- php: 5.4.45
- ruby: 1.8.7(かなり古い...)
測定結果
言語 | 時間(秒) |
---|---|
C言語 | 0.1 |
Elixir(再帰処理) | 0.9 |
Elixir(:lists.seq) | 1.5 |
Elixir(並列実行) | 2.6 |
python(reduce) | 2.8 |
python | 6.5 |
Elixir(直列実行) | 7.1 |
php | 11.5 |
ruby | 22.0 |
rubyは古すぎなので例外として、reduce() を利用した python は C言語で実装されてるロジックを呼び出している(はず)なので、1 core しか使ってないくせに、かなり早いです。
htop による CPU 利用状況はこんな感じです(1 core 系の言語はどれも同じなので php だけ)。Elixir で spawn した場合、各coreを利用しているのがわかります。
php
Elixir(直列実行)
Elixir(直列実行)
ソースコード
各言語のソースコードは GitHub にあります。Elixirの並列実行版だけ、下記に転記しておきます。
defmodule Sum do
def calc(from, to) do
Enum.reduce(from..to-1, fn(x, acc) -> x + acc end)
end
end
0..5000-1
|> Enum.map(&(Task.async(fn -> Sum.calc(10000*&1, 10000*(&1+1)) end)))
|> Enum.map(&(Task.await/1))
|> Enum.reduce(fn (x, acc) -> x+acc end)
|> IO.puts
【追試】
追試したソースコードは GitHub にあります。
Range を :lists.seq/2 に置き換え
速度に差異無し。Enum.map/2 内部の reduce() が呼ばれた時点で両者はほぼ同じ状態になるので当たり前か?
efmodule Sum do
def calc(from, to) do
Enum.reduce(from..to-1, fn(x, acc) -> x + acc end)
end
end
:lists.seq(0, 5000-1)
|> Enum.map(&(Task.async(fn -> Sum.calc(10000*&1, 10000*(&1+1)) end)))
|> Enum.map(&(Task.await/1))
|> Enum.reduce(fn (x, acc) -> x+acc end)
|> IO.puts
Enum.reduce/2 を :lists.sum/1 に置き換え
約40%の速度向上になりました。
defmodule Sum do
def sum_seq(from, to) do
:lists.seq(from, to - 1) |> :lists.sum
end
end
0..5000-1
|> Enum.map(&(Task.async(fn -> Sum.sum_seq(10000*&1, 10000*(&1+1)) end)))
|> Enum.map(&(Task.await/1))
|> Enum.reduce(fn (x, acc) -> x+acc end)
|> IO.puts
リスト処理を無くして再帰処理
約70%の速度向上になりました。
defmodule Sum do
def sum_loop(from, to) do
sum_loop1(from, to, 0)
end
defp sum_loop1(to, to, acc), do: acc
defp sum_loop1(from, to, acc) do
sum_loop1(from + 1, to, acc + from)
end
end
0..5000-1
|> Enum.map(&(Task.async(fn -> Sum.sum_loop(10000*&1, 10000*(&1+1)) end)))
|> Enum.map(&(Task.await/1))
|> Enum.reduce(fn (x, acc) -> x+acc end)
|> IO.puts
まとめ
- 簡単なソースコードで CPU core を有効に利用できるElixir(Erlang) すごい。
- C言語の圧倒的な演算速度を見ちゃうと、スクリプト言語は演算系の処理には向いてないことが良く分かる(確か、Erlang の作者も強みはそこじゃない、みたいなことを言っていた)。Erlangをスクリプト言語に入れていいかどうかはともかく。
- リスト処理はわりとコスト高なので、パフォーマンスが問題になった時には真っ先にチューニングしてみるといい。