DEV Community

loading...
Cover image for How do I improve performance in Elixir

How do I improve performance in Elixir

tokoyax profile image Takuya Aoki ・2 min read

https://dev.to/tokoyax/a-little-story-about-the-no-command-1am

Continuation of the above article

Execution speed problem

Command no is slower than theyes command like this.

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

I got the following comment.

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

It seems to be caused by a large number of I/O, so it is better to use a buffer.

Try to improve.

Using IO List

https://www.bignerdranch.com/blog/elixir-and-io-lists-part-1-building-output-efficiently

According to the above article, when passing strings to IO.puts, Instead of passing only one combined string, storing the part of the string to the list has better performance.

If the same string is stored in the list, it's efficiently because the same string refers to the same address.

I fixed 'no.exs' to use a buffer.

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

Measure

before

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

after

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

SUCCESS :D

Try parallel execution

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

Okay, let's run in parallel more.

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

Although the result which speeded up was obtained, the output result was not constant.

I don't know why.

Summary

  • I got knowledge of speed improvement such as IO List.
  • It is simply fun to make faster.
  • The yes command is still faster.

Github: [https://github.com/tokoyax/no:title]

(reference)

Discussion (0)

pic
Editor guide