Flowを使って、CPUのコアをフルに使えるか試してみました。
処理内容は、乱数で円周率を求める(適当な処理内容を思いつかなかったので地味な課題です)
乱数の生成、判定、カウント3段階のパイプにしています。各処理をEnum/Flowを使った場合で比較してみました。
結果
関数名 | 乱数生成 | 判定 | カウント | 実行時間 |
---|---|---|---|---|
calc_pi_with_enum | Enum | Enum | Enum | 5.065 |
calc_pi_with_flow | Flow | Flow | Enum | 0.690 |
calc_pi_with_flow_reduce | Flow | Flow | Flow | 0.475 |
5.065/0.475 = 10.66
10倍速くなりました
実行結果詳細
$ mix run bench/cutorial003_bench.exs
Operating System: Linux
CPU Information: AMD Ryzen 7 PRO 5750G with Radeon Graphics
Number of Available Cores: 16
Available memory: 15.29 GB
Elixir 1.13.4
Erlang 24.3.4.2
Benchmark suite executing with the following configuration:
warmup: 2 s
time: 5 s
memory time: 0 ns
reduction time: 0 ns
parallel: 1
inputs: none specified
Estimated total run time: 21 s
Benchmarking Enum ...
Benchmarking Flow (count) ...
Benchmarking Flow (reduce) ...
Name ips average deviation median 99th %
Flow (reduce) 2.10 475.37 ms ±3.28% 474.21 ms 500.76 ms
Flow (count) 1.45 690.81 ms ±4.28% 686.25 ms 742.75 ms
Enum 0.197 5065.95 ms ±0.00% 5065.95 ms 5065.95 ms
Comparison:
Flow (reduce) 2.10
Flow (count) 1.45 - 1.45x slower +215.44 ms
Enum 0.197 - 10.66x slower +4590.58 ms
テストプログラム
2022/08/20変更
処理時間の測定を:timer.tc()
で行っていましが、@zacky1972さんから、コメントいただいた、Bencheeを使った方法に変更し、再測定しました。
Bencheeのinstallもコメントいただいた、Mix.installを使った方法で試してうまくいったので、最終的には、mix.exsの方に入れました。コメントありがとうございました。
defmodule Tutorial003 do
def gen_rand(_) do
{:rand.uniform(), :rand.uniform()}
end
def calc_pi_with_enum() do
hit =
1..10_000_000
|> Enum.map(&gen_rand(&1))
|> Enum.map(fn {x, y} -> x * x + y * y < 1 end)
|> Enum.count(fn bool -> bool end)
hit / 10_000_000 * 4
end
def calc_pi_with_flow() do
hit =
1..10_000_000
|> Flow.from_enumerable()
|> Flow.map(&gen_rand(&1))
|> Flow.map(fn {x, y} -> x * x + y * y < 1 end)
|> Enum.count(fn bool -> bool end)
hit / 10_000_000 * 4
end
def calc_pi_with_flow_reduce() do
hit =
1..10_000_000
|> Flow.from_enumerable()
|> Flow.map(&gen_rand(&1))
|> Flow.map(fn {x, y} -> x * x + y * y < 1 end)
|> Flow.reduce(fn -> 0 end, fn entry, acc ->
if(entry, do: acc + 1, else: acc)
end)
|> Flow.on_trigger(fn count -> {[count], count} end)
|> Enum.sum()
hit / 10_000_000 * 4
end
end
Benchee.run(
%{
"Enum" => fn _ -> Tutorial003.calc_pi_with_enum() end,
"Flow (count)" => fn _ -> Tutorial003.calc_pi_with_flow() end,
"Flow (reduce)" => fn _ -> Tutorial003.calc_pi_with_flow_reduce() end
},
before_each: fn _ -> :rand.seed(:exsss, {100, 101, 102}) end
)
このほかのファイルは、Githubを参照ください。
https://github.com/masahiro-999/tutorial003
CPU使用率
実行中のCPU使用率です。
3つの処理が連続して実行されてるので、切り替わった時を、目視でざっく赤枠で記入しました。
まとめ
Enum/Flowの速度比較
- EnumとFlowの比較で10倍速くなった
- Flowを使うことで、CPUが80~90%動作するようになったことを確認できました。
- Enumの場合10~15%なので1コア分の処理能力(1/8)と思われます。
- Flowの場合は、8コアが動作して十分速い結果でした。
- コア数以上に速くなったのは、スレッドの効果(8コア16スレッドのCPU)なのか、Enumの場合に1CPUの能力をだせていない不利な要因があるのかもしれません。
Flowについて
- Flowを使えば、いい感じに、処理を分割していい感じに各CPUに割り振って動作します。
- Flow.map()は簡単に使えますた。
- Flow.map()だけでもそこそこ速くなりますが、結果を統合する部分をFlow.reduceをつかうと、もう一段速くなるかもしれない。
※@zacky1972さんの結果のように、CPUの種類や、バージョンによって速くなる度合いには違いがありそうです
論理プロセッサの使用率
@tenmyoさんから、コメントいただいた方法で、論理プロセッサの使用率もみてみました。左半分がEnumの時です。Enumの時には、一部の論理プロセッサーだけで動作していますが、Flowの場合は、すべて高くなってました。特に並列化を意識せず、ここまで処理が並列化されるのは素晴らしい。