LoginSignup
32
30

More than 5 years have passed since last update.

演算の並列実行でCPUコアを使い切るかどうかを測定

Last updated at Posted at 2015-12-25

【追記】
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(かなり古い...)

測定結果

bench2.png

言語 時間(秒)
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

php.png

Elixir(直列実行)

elixir1.png

Elixir(直列実行)

elixir2.png

ソースコード

各言語のソースコードは GitHub にあります。Elixirの並列実行版だけ、下記に転記しておきます。

elixir_concurrent.exs
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() が呼ばれた時点で両者はほぼ同じ状態になるので当たり前か?

sum1.exs
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%の速度向上になりました。

sum2.exs
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%の速度向上になりました。

sum3.exs
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をスクリプト言語に入れていいかどうかはともかく。
  • リスト処理はわりとコスト高なので、パフォーマンスが問題になった時には真っ先にチューニングしてみるといい。
32
30
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
32
30