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

Flowを使って、CPUのコアをフルに使えるか試してみました。

処理内容は、乱数で円周率を求める（適当な処理内容を思いつかなかったので地味な課題です）

乱数の生成、判定、カウント３段階のパイプにしています。各処理を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倍速くなりました

## 実行結果詳細

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

```Elixir: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
```

```elixir: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/532235/e763c978-5c17-c510-8322-d248c515e384.png)

# まとめ

Enum/Flowの速度比較

- EnumとFlowの比較で10倍速くなった
- Flowを使うことで、CPUが80~90％動作するようになったことを確認できました。
- Enumの場合10~15%なので１コア分の処理能力（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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/532235/a016dbba-8594-64ff-8c15-14208dd26a32.png)