LoginSignup
5
2

More than 1 year has passed since last update.

Elixir FlowでRyzenの全コアぶん回したら10倍速かった

Last updated at Posted at 2022-08-19

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の方に入れました。コメントありがとうございました。

tutorial003.ex
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
tutorial003_bench.exs
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つの処理が連続して実行されてるので、切り替わった時を、目視でざっく赤枠で記入しました。

flowcpuusage2.png

まとめ

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の場合は、すべて高くなってました。特に並列化を意識せず、ここまで処理が並列化されるのは素晴らしい。

image.png

5
2
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
5
2