fugafuga.write

日々のログ

'no'コマンドについての本当にちょっとした話 ―速度改善編―

blog.tokoyax.com

この記事の続き

速度の問題

> yes | pv -r > /dev/null
[31.0MiB/s]
> ./no | pv -r > /dev/null
[85.6KiB/s]

自作のコマンドnoはこのようにyesコマンドに比べて遅い。

英語の練習も兼ねて dev.to に同じ記事を投稿したところ、以下のようなコメントをいただけた。

you've better to use buffer for writing. Actually, yes command does not call write(2) for each lines.

なるほど、I/Oの回数が多いことが原因と思われるのでバッファを使うほうがよいとのこと。

改善してみる

IO List を使う

Elixir and IO Lists, Part 1: Building Output Efficiently

この記事によると、文字列を結合しながらIO.putsに渡す場合、
結合された文字列を1つだけ渡すのではなく、結合前の文字列のパーツをリストに格納するほうがパフォーマンスが良いとのこと。

リスト内に同じ文字が格納されている場合、同じ文字は同じアドレスを参照するため効率がよいらしい。

バッファリングするようにしたものがこちら。

no.exs

defmodule No do
  @moduledoc """
  Documentation for No.
  """

  def main(args) do
    args
    |> parse_args()
    |> run()
    |> wait()
  end

  def say(args) do
    {_, _, _, debug} = args
    message = buffer(args)
    case debug do
      true -> IO.inspect({:debug, message, self()})
      _    -> IO.puts(message)
    end
    say(args)
  end

  defp buffer(args) do
    {argv, process_num, _, debug} = args

    message = case debug do
      true -> "#{expletive(argv)} from process no.#{process_num}"
      _    -> expletive(argv)
    end

    buffer(args, message, [])
  end

  defp buffer(args, message, list) do
    # faster than append last
    # https://hexdocs.pm/elixir/List.html
    list = [message | list]
    cond do
      IO.iodata_length(list) >= 4096 -> list
      true -> buffer(args, message, list)
    end
  end

  defp expletive(_ = ''), do: "n\n"
  defp expletive(value), do: value

  defp parse_args(args) do
    {opts, argv, _} = OptionParser.parse(
      args,
      swithces: options_list(),
      aliases: aliases_list()
    )
    {argv, opts}
  end

  defp options_list() do
    [
      processes: :integer,
      debug: :boolean,
    ]
  end

  defp aliases_list() do
    [
      p: :processes,
      d: :debug,
    ]
  end

  defp run(args) do
    {argv, opts} = args

    num = case opts[:processes] do
      nil -> 1
      _   -> String.to_integer(opts[:processes])
    end

    Enum.each((1..num), fn(n) ->
      spawn_link(No, :say, [{argv, n, self(), opts[:debug]}]) end)
  end

  defp wait(args) do
    wait(args)
  end
end

やったこと

  • List は要素を先頭に追加するほうが速いのでそのように
  • 親プロセス側でメッセージを待つのをやめて各子プロセスで出力するように

測定する

before

> ./no | pv -r > /dev/null
[85.6KiB/s]

after

> ./no | pv -r > /dev/null
[ 802KiB/s]

SUCCESS 😇 😇 😇 😇 😇 😇 😇 😇 😇 😇

並列実行してみる

> ./no -p 4 | pv -r > /dev/null
[1.33MiB/s]

単位が上がりましたね....これで無限にプロセスを増やせば....

> ./no -p 10000 | pv -r > /dev/null
[5.00MiB/s]
[3.16MiB/s]
[1.84MiB/s]
[ 458KiB/s]
[ 466KiB/s]
[ 569KiB/s]
[ 724KiB/s]
[ 805KiB/s]
[1.27MiB/s]
[6.17MiB/s]
[15.3KiB/s]
[1.13MiB/s]

ブレがすごい

まとめ

  • IO List など速度改善の知見が得られた
  • 自分で作ったものが速くなるのは単純に楽しい
  • まだyesには全然敵わないので速度改善の余地はありそう
  • ブレがすごい

ブレについては気が向いたら調べる

Github: GitHub - tokoyax/no: Say No!!!!

プログラミングElixir

プログラミングElixir

(参考)