やわらかテック

興味のあること。業務を通して得られた発見。個人的に試してみたことをアウトプットしています🍵

【レポート】第10回清流elixir勉強会を開催しました【Elixirで速度測定】

トピック

今回で第10回目の勉強会を開催致しました
elixir-sr.connpass.com

3月の下旬に始めた当勉強会も、気づけば10回もやってたのかと感慨深い
「どうやって運営してるんですか?」とよく聞かれることがあるが別に何か特別なことはしていないはず
ただ、自分の知りたいことをテーマとして会場に持ち込んだり(ある程度アジェンダは組む)
参加者の方から気になってることを聞いて、調査しながら手を動かしたりと

ポイントは毎回のテーマの濃さよりも、とりあえずは続けることだと考えている
続けている内に内容も濃くなっていくはず

今回は前々から気になっていた
パターンマッチによる条件分岐とif, caseのような制御構文とどっちが速いのかを測定してみた
Elixirのお作法としてはパターンマッチを推奨しているが、実際は速度はどうなんやろと単純な疑問にて開催

清流elixir-infomation
開催場所: 丸の内(愛知)
参加人数: 5 -> 5
コミュニティ参加人数 : 13 -> 13
20190810現在

第10回の勉強会の内容について

Elixirに用意されている制御構文について

f:id:takamizawa46:20190810091037j:plain
パターンマッチによる条件分岐を推奨しているもののElixirにも制御構文は用意されている
それぞれについて詳しい説明はしないが、ざっくりと列挙する

  • if else(else if は無い)
  • unless
  • case
  • cond

ifやcase(jsでいうswtichに近しい)については説明するまでもないので省略
unlessとcondについては私は普段全くと言っていいぐらい使っていないので、今回初めて触れることになった

unlessについて

プログラミングElixirではunlessについては色々と酷い扱いがされている
実際のユースケースが私にも想定できない。not使えばええやんって
どうやらrubyではnotを使うことを推奨していないようでunlessを使う機会があるらしい
どっちにしろ極力使わないようにしようと思っている

unless true do
  IO.puts("--> True")
else
  IO.puts("--> False")
end

# result
--> True

condについて

condはconditionの略なんやろうな〜と思ってる。caseに似ているが条件式を複数個記述することが可能
以前、入門会で書いたFizzBazzもcondを使うとこんな感じにまとめる

num = 15

cond do
  rem(num, 15) == 0 -> IO.puts("FizzBuzz")
  rem(num, 3) == 0 -> IO.puts("Fizz")
  rem(num, 5) == 0 -> IO.puts("Bazz")
  true -> IO.puts("another")
end

# result
FizzBuzz

場合によっては使い所がありそうな気がするが、パターンマッチで条件分けが可能なので個人的な使用率はかなり低い
Elixirの制御構文について3段階で総合評価(max=3)をそれぞれにすると(個人的主観)

制御構文 かきやすさ 使用頻度
if 3 3
case 2 2
unless 3 1
cond 2 1

Elixirでの速度測定について

f:id:takamizawa46:20190810091044j:plain
速度の測定についてはElixirでの実行速度の測定と色々と実験してみた【Enun.sum vs Enum.reduce etc...】にまとめたのでこちらを参照してほしい

ざっくりとだけまとめておくと、Elixirには速度測定のための関数が用意されていないためErlangの:timer.tcという関数を使用する
:timer.tcには3種類の引数パターンがある

  • :timer.tc(function)
  • :timer.tc(function, [arguments])
  • :timer.tc(module, :function, [arguments])

戻り値は2つの要素をもつタプルになっている

{sec, result} = :timer.tc(function)

速度を測定してみる

ここまで準備が整ったところで、さっそく今回の目標である
「パターンマッチと制御構文どっちがはやいねん」を検証する

比較するモジュールは以下。めっちゃシンプルな受け取った果実名に対応する値段を返すのみ
リストを受けて測定をするためにhelper関数(helper_for_price)をそれぞれに用意している

# pattern match
defmodule Sample do
    def helper_for_price(lst) do
        Enum.map(lst, fn f -> price(f) end)
    end
    def price("apple") do
        110
    end
    def price("banana") do
        70
    end
    def price("orange") do
        120
    end
 end
 
# 制御構文(case)
 defmodule Sample2 do
    def helper_for_price(lst) do
        Enum.map(lst, fn f -> price(f) end)
    end
    def price(fruit) do
        case fruit do
            "apple" -> 110
            "banana" -> 70
            "orange" -> 120
        end
    end
 end

とりあえず記念(何の)に1度、それぞれを測定してみる

{sec1, _res} = :timer.tc(Sample, :price, ["apple"])
{sec2, _res} = :timer.tc(Sample2, :price, ["apple"])

IO.puts("pattern match: #{sec1}")
IO.puts("case: #{sec2}")

# result ----
# pattern match: 1
# case: 0

早すぎてcaseが0になってて草
単位はマイクロ秒なので恐ろしく速い
次に100個の要素を持つリストをhelper関数に渡して測定してみる
そのためにランダムでfruit名("apple" or "orange" or "banana")を指定個数分、要素にもつリストを用意する必要があるため
Enumを使ってさくっと作成

lst_size = 10
fruits = ["apple", "banana", "orange"]
fruits_lst = Enum.map(1..lst_size, fn _ -> Enum.random(fruits) end)
IO.inspect(fruits_lst)

# result
# ["orange", "apple", "banana", "banana", "orange", "apple", "banana", "orange", "apple", "apple"]

それぞれのモジュールのhelper関数に作成したリストを渡して速度を測定する
リストの要素数は適当に100000にした

lst_size = 100000
fruits = ["apple", "banana", "orange"]
fruits_lst = Enum.map(1..lst_size, fn _ -> Enum.random(fruits) end)

{sec1, _res} = :timer.tc(Sample, :helper_for_price, [fruits_lst])
{sec2, _res} = :timer.tc(Sample2, :helper_for_price, [fruits_lst])

IO.puts("pattern match: #{sec1}")
IO.puts("case: #{sec2}")

# result
# pattern match: 13832
# case: 9151

先ほどはほとんど差がなかった要素数が増えると如実に結果に現れた
ほう、何と制御構文の方が速いではないか
同じ条件で測定を100回行い平均値を算出してみる

lst_size = 100000
fruits = ["apple", "banana", "orange"]
fruits_lst = Enum.map(1..lst_size, fn _ -> Enum.random(fruits) end)


try_num = 100

sec1s = Enum.map(1..try_num, fn _ -> :timer.tc(Sample, :helper_for_price, [fruits_lst]) end)
        |> Enum.map(fn {sec, _} -> sec end)
        |> Enum.sum()

sec2s = Enum.map(1..try_num, fn _ -> :timer.tc(Sample2, :helper_for_price, [fruits_lst]) end)
        |> Enum.map(fn {sec, _} -> sec end)
        |> Enum.sum()
        
IO.puts("pattern match: average -> #{sec1s / try_num}")
IO.puts("case: average -> #{sec2s / try_num}")

# result
# pattern match: average -> 22682.64
# case: average -> 20379.93

やはり若干だが、caseの方が速いよう
ただほぼ誤差程度だと言えるので、だからパターンマッチよりも制御構文使った方が良いかというとそうではないと考える
公式がパターンマッチによる分岐を推奨しているので脳死で私はそちらを選択する
ただ、こうして数値として出してみることで考える機会になったので非常によかった

次回について

まだ内容について特に決まっておらず、現在模索中
そろそろ並列処理をやってもいいかなとも思っているし、phoenixをやりたいとも思っている
興味のあることや、気になるテーマがあればコメント、twitterなどでご意見を頂ければと思います
バラエティみたく気になるテーマを調査しますので

fukuoka.exさんのもくもく会にリモートで参加させて頂きました【Phoenix入門】

参加したもくもく会

以前から合同で勉強会をさせて頂いていたり、様々なことを教えて頂いているfukuoka.exさんのリモート勉強会に再び参加させて頂きました
名古屋には多くの勉強会があるとは言える状況ではないですが、リモートで接続可能な勉強会があるというのはありがたいことです

fukuokaex.connpass.com

また今回のもくもく会で以前よりtwitterでよしなにして頂いているElixirの先駆者piacereさんに初対面致しました

twitter.com

いつも「いいね、RT」頂きまして本当に有難うございます!!

今回も経験者トラックで参加しておりますが、選んだテーマはPhoenix入門
PhoenixでjsonAPIを作成したことはあるんですが、ページビュー含めて開発したいなと思ってこのテーマを選定
謎のエラーがでる & ほとんど清流elixirのメンバーと雑談しつつ、もくもくしていた(?)のであまり進めることは出来なかった

何とかphoenixのwelcome画面まで進めることが出来た

Phoenix大入門

いまさらながらPhoenixでプロジェクトを始めるまでの手順をまとめてみる
元々、elixirは自分の環境にインストール済みだったのでphoenixをインストールする(あれ、インストールしてたはずなんやが...)

$ mix archive.install https://github.com/phoenixframework/archives/raw/master/phx_new.ez

無事にインストールが完了したっぽい

$ mix phx.new -v
Phoenix v1.3.4

さっそくプロジェクトを作成してみる
今回は以下の記事をもとにミニマムブログを作成してみる

qiita.com

プロジェクトの名前はmin_blogとした
コマンドを実行する

$ mix phx.new min_blog

お、色々と生成されて無事にプロジェクトが始められたっぽい

* creating test/config/config.exs
* creating test/config/dev.exs
* creating test/config/prod.exs
* creating test/config/prod.secret.exs
* creating test/config/test.exs
* creating test/lib/test/application.ex
* creating test/lib/test.ex
:
:
* creating test/assets/static/robots.txt
* creating test/assets/static/images/phoenix.png
* creating test/assets/static/favicon.ico

依存関係をインストールするかと聞かれているので「Y」と入力してお願いする
Yを選択すると

mix deps.get
:
mix deps.compile

のコマンドが走り、必要な外部ライブラリをDLしてくれる
DLが終了して「さぁPhoenixサーバーを立ち上げてくれよな」という文言が現れる

    $ cd min_blog

Then configure your database in config/dev.exs and run:

    $ mix ecto.create

Start your Phoenix app with:

    $ mix phx.server

You can also run your app inside IEx (Interactive Elixir) as:

    $ iex -S mix phx.server

言われる通りにコマンドを実行していく

$ cd min_blog
$ mix ecto.create

おや... 謎のerrorが...

** (Postgrex.Error) FATAL 28000 (invalid_authorization_specification): role "postgres" does not exist
    (db_connection) lib/db_connection/connection.ex:163: DBConnection.Connection.connect/2
    (connection) lib/connection.ex:622: Connection.enter_connect/5
    (stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3
Last message: nil
State: Postgrex.Protocol
** (Mix) The database for MinimumBlog.Repo couldn't be created: FATAL 28000 (invalid_authorization_specification): role "postgres" does not exist

なんか怒られた
postgresqlはインストール済みであることを確認

$ psql --version
psql (PostgreSQL) 11.2

どうやら「postgres」というroleが生成されていないようなので急いで作成する

$ createuser -d postgres

再度、dbをcreateするためにコマンドを実行する
またエラーかい〜〜

$ mix ecto.create
warning: found quoted keyword "min_blog" but the quotes are not required. Note that keywords are always atoms, even when quoted. Similar to atoms, keywords made exclusively of Unicode letters, numbers, underscore, and @ do not require quotes
  mix.exs:57

The database for MinimumBlog.Repo has been created
➜  minimum_blog mix phx.server
warning: found quoted keyword "min_blog" but the quotes are not required. Note that keywords are always atoms, even when quoted. Similar to atoms, keywords made exclusively of Unicode letters, numbers, underscore, and @ do not require quotes
  mix.exs:57

warning: please add the following dependency to your mix.exs:

    {:plug_cowboy, "~> 1.0"}
:
:
[info] Application minimum_blog exited: MinimumBlog.Application.start(:normal, []) returned an error: shutdown: failed to start child: MinimumBlogWeb.Endpoint
    ** (EXIT) shutdown: failed to start child: Phoenix.Endpoint.Handler
        ** (EXIT) "plug_cowboy dependency missing"
** (Mix) Could not start application minimum_blog: MinimumBlog.Application.start(:normal, []) returned an error: shutdown: failed to start child: MinimumBlogWeb.Endpoint
    ** (EXIT) shutdown: failed to start child: Phoenix.Endpoint.Handler
        ** (EXIT) "plug_cowboy dependency missing"

あーこれは分かりやすいエラー。外部ライブラリが不足している様だ
./mix_blog/mix.exs/に以下を追加して再び外部ライブラリをDLする

defp deps do
    [
      :
      :
      {:cowboy, "~> 1.0"},
      {:plug_cowboy, "~> 1.0"}
    ]
  end
$ mix deps.get

3度目の正直。ecto.createを実行する

$ mix ecto.create
Generated min_blog app
The database for min_blog.Repo has been created

やったぜ。phoenixサーバーを立ち上げる

$ mix phx.server
[info] Running TestWeb.Endpoint with cowboy 2.6.3 at 0.0.0.0:4000 (http)
[info] Access TestWeb.Endpoint at http://localhost:4000

ようやくwelcome画面とご対面
f:id:takamizawa46:20190803144405p:plain:w700

ここまで長かった
docker-composeを使って楽すれば良かったと後悔

お知らせ

いつもお世話になっているfukuoka.exさんが小倉城という城(すごすぎ)をジャックして
ElixirConf JP 2019 Kokurajo~を開催されます

fukuokaex.connpass.com

私は現在お財布と予定とにらめっこ中です
会社からElixir関連では経費が落ちないそうです。くぅ~

【Enun.sum vs Enum.reduce】Elixirでの実行速度の測定と色々と実験してみた

測定に至る背景

再来週に開催する清流elixirの勉強会でifとパターンマッチでそれだけ実行速度に差が出るのかを測定しようと企画している
そのために自身の予習を兼ねて、Elixirでの実行速度の測定方法について調査し、簡単な実験を行なってみた
以前から気になっていた

  • Enum.sum()
  • Enun.reduce()

上記2つはどちらが速いのか(sumはreduceで記述可能なため) 再帰関数とEnun.map()はどっちが速いのかなど...

色々と速度比較してみたのでまとめていく

Elixirでの実行速度の測定方法

Elixirには実行速度を測定するような関数は用意されていない
そのためErlangの:timer.tcという関数をcallする :timer.tcには以下のように

  • :timer.tc(function)
  • :timer.tc(function, [arguments])
  • :timer.tc(module, function, [arguments])

引数違いで3種類用意されている
戻り値はそれぞれ

{micro second, result}

のタプル形式になっている

マイクロ秒(マイクロびょう、microsecond、記号: µs)は、100万分の1秒(10−6 s, 1/1,000,000s
https://ja.wikipedia.org/wiki/%E3%83%9E%E3%82%A4%E3%82%AF%E3%83%AD%E7%A7%92

順に適当なサンプルを使って実行速度を測定してみる
今回は1..100のレンジの各要素を2倍にしてリストで返してみる

引数が1つの:timer.tc(function)

無名関数を作成して渡してやれば良い
無名関数の引数はなしにしてあげれば問題なくパスできる

:timer.tc(fn -> Enum.map(1..100, &(&1 * 2)) end)
{175,
 [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40,
  42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78,
  80, 82, 84, 86, 88, 90, 92, 94, 96, ...]}

最初、無名関数に「 _ 」の引数を用意してしまっていた割とハマった
引数なしでも実行できるの知らなかったので勉強になった

fn -> IO.puts("こんな書き方あったんかい!") end

以下の書き方ではerrorになる
「 _ 」でも引数として認識されている模様

:timer.tc(fn _ -> Enum.map(1..100, &(&1 * 2)) end)

** (BadArityError) #Function<6.128620087/1 in :erl_eval.expr/5> with arity 1 called with no arguments
    (stdlib) timer.erl:166: :timer.tc/1

無名関数に引数を与えてはいけないんだね

引数が2つの:timer.tc(function, [arguments])

先ほどとは異なり、無名関数に引数を用意して:timer.tcの第2引数から渡してもらえるようにする

:timer.tc(fn lst -> Enum.map(lst, &(&1 * 2)) end, [1..100])
{229,
 [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40,
  42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78,
  80, 82, 84, 86, 88, 90, 92, 94, 96, ...]}

引数が3つの:timer.tc(module, function, [arguments])

まずはモジュールと関数を適当に用意する

defmodule Sample do
  def calc(enum_) do
    enum_ |> Enum.map(&(&1 * 2))
  end
end

第2引数の渡し方に注意。Erlangドキュメントに記載があるように関数はアトム型として待ち構えている
そのため「:関数名」という形式で記述をする必要がある。ここで割とハマった

Erlang Reference Manual Version 3.9より Types
Module = module()
Function = atom()
Arguments = [term()]
Time = integer()
In microseconds
Value = term()

:timer.tc(Sample, :calc, [1..100])
{26,
 [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40,
  42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78,
  80, 82, 84, 86, 88, 90, 92, 94, 96, ...]}

第2引数がアトム型だと気づかずerrorを出しまくったパターン達

:timer.tc(Sample, by, [[1,2,3,4,5], []])
:timer.tc(Sample, by(), [[1,2,3,4,5], []])
:timer.tc(Sample, by/2, [[1,2,3,4,5], []])
:timer.tc(Sample, Sample.by/2, [[1,2,3,4,5], []])

色々、試したけど全部ダメ。やはり公式ドキュメント最強

色々と実験

実行速度の測定方法も何となく分かったところで早速、実験をやってみようと思う
今回試してみるのは以下

  • Enum.reduce()の引数2つと引数3つはどっちが速いのか
  • Enum.max()とEnum.reduce()はどっちが速いのか
  • 再帰関数とEnum.map()はどっちが速いのか

を100回の実行での平均値をとってそれぞれ比較してみる
グラフの描画にはpythonのmatplotlibを使用した
適当な関数を作ったので一応、貼っておく

def plot_result(result_ary, try_num, title, x_label, y_label, is_gird=True, is_average_show=True):
    """
        draw result graph from result array and total try
        >>> result_ary = [12, 43, 23, 34, 54] # free size
        >>> try_num = 5 # must: result_ary size == try_num
        >>> title = "Result time"
        >>> x_label = "try"
        >>> y_label = "time(micro second)
        >>> is_grid = True # default: True, show grid in graph?
        >>> is_average_show = True #default: True, show average line in graph?
        >>> plot_result(result_ary, try_num, title, x_label, y_label)
        create graph, use matplotlib
    """
    if len(result_ary) != try_num:
        raise ValueError(f"not match {len(result_ary)} size != {try_num}")
    
    try_num_lst = [num for num in range(try_num)]    
    plt.plot(try_num_lst, result, lw=3, label="result")
    
    if is_average_show:
        average = sum(result) / len(result)
        average_lst = [average for _ in range(try_num)]
        plt.plot(try_num_lst, average_lst, 'g--', color="r", lw=2, label='average')
        max_val = max(result_ary)
        plt.text(5, max_val - max_val * 0.1, f"average: {average}", size = 15, color = "green")

    plt.title(title)
    plt.xlabel(x_label)
    plt.ylabel(y_label)

    plt.legend()
    plt.grid(is_gird)
    plt.savefig(f'{title}.png')
    plt.show()

筆者のPCスペック

  • MacBook Pro (13-inch, 2016, Two Thunderbolt 3 ports)
  • プロセッサ: 2GHz Intel Core i5
  • メモリ: 8GB 1867 MHz LPDDR3
  • グラフィックス: Intel Iris Graphics 540 1536 MB

1.Enum.reduce()の引数2つと引数3つ対決

1から100までの数値を合計する処理で2つを比較してみる

# 引数が2つ
Enum.reduce(1..100, fn x, acc -> x + acc end)

# 引数が3つ
Enum.reduce(1..100, 0, fn x, acc -> x + acc end)

100回の実行はEnun.mapを使って実行している。そして結果を描画する
引数が2つの場合

res = Enum.map(1..100, fn _ -> 
  :timer.tc(fn -> Enum.reduce(1..100, fn x, acc -> x + acc end) end)
end)
|> Enum.map(fn {s, _} -> s end)

f:id:takamizawa46:20190728120636p:plain

引数が3つの場合

res = Enum.map(1..100, fn _ -> 
  :timer.tc(fn -> Enum.reduce(1..100, 0, fn x, acc -> x + acc end) end)
end)
|> Enum.map(fn {s, _} -> s end)

f:id:takamizawa46:20190728121134p:plain

accumlatorの初期値を渡さない引数2つのパターンの方が速い
たまに発生する急激な処理速度の低下は何なんだろう。音楽流しながら測定してるのが関係あるか?

2.Enum.max()とEnum.reduce()の合計対決

Enum.max()

res = Enum.map(1..100, fn _ -> 
  :timer.tc(fn -> Enum.max(1..100) end)
end)
|> Enum.map(fn {s, _} -> s end)

f:id:takamizawa46:20190728121551p:plain

Enum.reduce()でmaxを算出

# Enum.reduce() 先ほどの検証で分かった高速な引数3のreduce()を使用
res = Enum.map(1..100, fn _ -> 
  :timer.tc(fn -> Enum.reduce(1..100, 0, fn x, acc ->
     if x > acc do
       x
     else
       acc
     end
  end) end)
end)
|> Enum.map(fn {s, _} -> s end)

f:id:takamizawa46:20190728121925p:plain

なんとEnum.max()よりもEnum.reduce()の方が速いではないか
速度の最大値(μs)と最小値(μs)でこれだけ差が開くのはなんでなんだろうか
しかし、今のmax値の探索は100が当然ながら答えなので最大長(O(n))の結果となっているため
自身で用意したランダムなリストでどのような結果になるのかを確かめてる

指定範囲内で指定回数分のランダム数値を持つリストを作成する無名関数を用意した

random_creater = fn total_elem, max -> Enum.map(total_elem, fn _ -> :random.uniform(max) end) end
random_creater.(1..100, 100) 
iex(116)> random_creater.(1..100, 100)
[18, 95, 80, 85, 95, 5, 41, 29, 78, 89, 7, 88, 22, 85, 14, 50, 29,
 42, 29, 98, 7, 27, 48, 18, 62, 74, 76, 89, 85, 71, 82, 27, 21, 29,
 32, 3, 22, 65, 1, 36, 6, 59, 61, 12, 49, 97, 5, 19, 71, 52, ...]

同様にそれぞれ結果を見てみる
f:id:takamizawa46:20190728213615p:plain
f:id:takamizawa46:20190728214248p:plain

なんじゃこりゃ。ランダムな要素で作成したリストで同じ処理をしたら
先ほどと結果が逆転して、かなりの差がついた
もしかしてレンジを使っていたのが良くなかったのかもしれない(Enum.reduce()の方がレンジの処理性能が良い?)

3.再帰関数とEnum.map()はどっちが速いのか

先ほどの結果も踏まえて、ランダな要素を持つ配列を使用する
全ての要素を2倍にする実行速度を比較してみる

再帰関数

defmodule Sample do
  def by([], accum), do: accum
  def by([head | tail], accum) do
    by(tail, accum ++ [head * 2])
  end
end

res = Enum.map(1..100, fn _ -> 
  :timer.tc(Sample, :by, [lst, []])
end) |> Enum.map(fn {s, _} -> s end)

f:id:takamizawa46:20190728215325p:plain

Enum.map()

res = Enum.map(1..100, fn _ ->
  :timer.tc(fn -> Enum.map(lst, &(&1 * 2)) end)
end) |> Enum.map(fn {s, _} -> s end)

f:id:takamizawa46:20190728215353p:plain

圧倒的に再帰関数の方が早かった
Elixir&ErlangFestでzackyさんからLTのあった通りの結果になった
とは言いつつも、Enum.map()の方が記述もしやすく手軽なのは紛れもない事実だ

普段、何気なく書いている処理の性能を比較してみると面白い
今回選定したテーマは割と強引に持ってきた感があるが、普段記述に迷うような処理があれば
Elixirであれば:timer.tc()を使って手軽に試せるので、実測してみるのも良い

参考文献

Erlang Reference Manual Version 3.9
Simplicity
Elixirで関数の実行速度を測定する
Elixirでパフォーマンス測定
Elixir Zen スタイルプログラミング講座

【レポート】第9回清流elixir勉強会を開催しました【ウェルカムElixir入門会】

トピック

今回で第9回目の勉強会を開催致しました
elixir-sr.connpass.com

東京で参加したElixir&ErlangFest2019やfukuoka.exさんの勉強会にリモート参加させて頂いた中で
コミュニティの盛り上がり、参加する人の多さが非常に重要であると痛感した

fukuokaex.connpass.com

それもそのはず。3人集まれば文殊の知恵と言うように
多くの人が集まればそれだけ多くの知識、アイディアが生まれる

同じ学習でも自分以外を交えて知識を交換しつつ学ぶ方が明らかに効率が良い

またElixirを4ヶ月程触る中で、いわゆる「良さ」というものを身にしみて理解することが出来た
この素晴らしい仕様と快適さを多くの人に体験してほしいと思った

そんな最もらしいこともあり、今回はElixirの入門会を開催した
結果、新規の方に2名参加していただき、Elixirの良さを伝える機会を頂くことが出来た
スピーカーとして自分は100点ではないのが申し訳ないが、良さは伝えられてたと思っている

清流elixir-infomation
開催場所: 丸の内(愛知)
参加人数: 2 -> 5 update!
コミュニティ参加人数 : 10 -> 13 update!
20190719現在

過去最高の参加者!! ありがとうございます!!

第9回の活動内容

Elixirの入門会を開催した
今までの勉強会の内容を振り返りつつ軽く手を動かしながら進めた
当日のアジェンダは以下

  • やさしいElixir -> Elixirって何やねん。化粧品? ポーション?
  • Elixirの特徴と機能的な話
  • なんで僕がElixirに興味を持っているか
  • Elixirのパワフルなシンタックス

やさしいElixir

まずは新規の参加者のお二人になぜElixirに手を出そうと思ったのかを聞いてみた
普段はpythonなどをメインに書かれており、どっから情報仕入れたのかなと思った

「名前がかっこいいからやろうと思った」

素晴らしいセンスを持っていらっしゃった
当然、Elixirという名前からファイナルファンタジーを連想するが前にもお話した通り
Elixirの作者Joseはファイナルファンタジーをプレイしたことが無いため、関係はない

定番の決まり文句でまずは場を盛り上げつつ、Elixirの概要に触れていく

  • Elixirって何?(全快のポーションではない) 2011年に誕生 -> まだ8歳
  • 2019年にわざわざ学ばなくてもいいプログラミング言語ランキングに7位でランクイン
  • 東海地方ではあまり名を聞くことのないプログラミング言語(2015年あたりに一度バズりがあったらしい) -> Qiitaの記事を種火にRuby勢が盛り上げた

Elixirの特徴と機能的な話

  • Erlangの仮想環境上(BEAM)で動作している
  • Erlangの機能が使用可能 -> 大量の独自プロセスを作成&管理可能
  • Erlang同様に非常に堅牢で、まず落ちない -> 任天堂switchの通知システムは2年間の間、一度も落ちていない(Erlang)
  • ErlangRubyシンタックスの良さを組み合わせ、Erlangの書きにくさが解消された
  • プロセス単位のガベージコレクタでメモリの使い方がかなりエコ
  • 大量の並行処理が気軽に記述可能
  • 終了したプロセスは消してしまうという考えなため、プロセスを再起動させて...なんてことをしなくていい
  • パワフルなシンタックス

Erlangの良さがつらつらと書かれているが、僕はErlang経験がないのでElixirから感銘を受けたのは並行処理、パワフルなシンタックスの部分である
なので上ら辺に書かれているErlangの強みがという部分に確信した情報を持っていないので一度、Erlangドキュメントに目を通さねばと思った
ErlangのOTPの仮想環境(BEAM)を一種のOSのように捉えていたが、本当にそうなのか?という議論になり非常に勉強になった

近日、勉強をしていたコンピューターサインエンスの話と繋がり、やっていてよかったと痛感
.exもしくは.exsの形式で保存されたファイルに記述された文字はElixirもしくはErlangによってコンパイルされ
BEAM(VM)上でbinarycodeに変換され実行されるという認識に落ち着いた
そのためElixirはErlangの機能を使用することが可能であり、OSに依存しない独自のプロセスが使えるということなのではと

なんで僕がElixirに興味を持っているか

このテーマに関しては過去の記事で何度も扱っているので、ざっくりとだけ記述しておく
より文字を読みたい方は以下のリンクを参照してほしい

www.okb-shelf.work

当日はこんな感じで話した(かな

  • 名前がかっこよい
  • 日本(特に東海では先駆者がいない) ではマイナー
  • 関数型言語をやってみたかった
  • 大量のプロセスによる並列処理がビックデータの処理にぴったり

Elixirのパワフルなシンタックス

ここでいうパワフルという意味は「少ない記述で高度な事が可能」という意味で扱っている
まずはEnumの関数達。入門という事で扱ったのは「map, filter, reduceの3つ」
そもそもElixirというか一般的に関数型言語にはfor文(繰り返し処理)が用意されていない
そんな時に使うのがElixirであればEnumの関数(内包表記など)
それぞれの使い方は以下の記事で紹介しているので詳しい話を以下のリンクを参照してほしい

話の落ちどころとしては、オブジェクト言語や手続き型言語で記述していたfor文を使った処理は
全て、Enum.map, Enum.filter, Enum.reduceなどの組み合わせで記述が可能であるということ
(まとめは参加者の方のお言葉をパクリスペクトさせて頂きました)

Enum.map, Enum.filterについて
www.okb-shelf.work

Enum.reduceについて(アキュムレーターという考え方がmapやfilterと異なるため、別に展開)
www.okb-shelf.work

Enumの関数は列挙可能なデータ構造に対して使用可能
列挙可能なデータ構造は以下

  • リスト
  • マップ
  • レンジ

今回は例としてリストを対象に手を動かした
まずはmapとfilterから

lst = [1,2,3,4,5,6,7,8,9,10]

# map: 全ての要素を2倍する
Enum.map(lst, fn num -> num * 2 end)
# [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

# filter: 7より大きい要素を破棄
Enum.filter(lst, fn num -> num < 7 end)
# [1, 2, 3, 4, 5, 6]

mapやfilterはリストを受け取ってリストを出力するため、データの型の変換を行うことが出来ない
そこで役立つのがreduce関数。accumlatorの考え方は上記のリンクをみて欲しい
以下の例ではリストの合計値を求めている。つまりはリストから数値への変換を行なっている

lst = [1,2,3,4,5,6,7,8,9,10]

# 要素の合計値を求める(リスト -> 数値)
Enum.reduce(lst, 0, fn num, acc -> num + acc end)
# 55

うん、すごい
しかし、単純にmap, filter, reduceはpythonにもある
でもElixirはもっとすごい。パイプ演算子という文法が用意されている
上記の3つの処理を順次pythonで行おうとするとこんな感じに

from functools import reduce

lst = [1,2,3,4,5,6,7,8,9,10]
res = map(lambda x: x * 2, lst)
res = filter(lambda x: x < 7, res)
res = reduce(lambda x, acc: x + acc, res)
print(res)
# 12

悪くはないが、都度、変数resに値を渡して、次へ次へと面倒臭い
Elixirであればパイプ演算子を使ってこんな風に記述することが出来る

lst = [1,2,3,4,5,6,7,8,9,10]

Enum.map(lst, fn num -> num * 2 end)
|> Enum.filter(fn num -> num < 7 end)
|> Enum.reduce(fn num, acc -> num + acc end)
|> IO.puts() 
# 12

何をやっているかが一目瞭然で、流れがよく分かる
不要になった部分はコメントアウトして以下のようにすれば良いし、新たに処理を追加するときも楽々だ

lst = [1,2,3,4,5,6,7,8,9,10]

# 処理を減らす
Enum.map(lst, fn num -> num * 2 end)
# |> Enum.filter(fn num -> num < 7 end)
|> Enum.reduce(fn num, acc -> num + acc end)
|> IO.puts()
# 110

# 処理を追加
Enum.map(lst, fn num -> num * 2 end)
|> Enum.filter(fn num -> num < 7 end)
|> Enum.map(fn num -> (num + 10) * 4 end)
|> Enum.reduce(fn num, acc -> num + acc end)
|> Integer.to_string()
|> IO.puts()
# 168

最後にパターンマッチについて触れた
(今回は関数の引数でのパターンマッチとガード説を用いた例を紹介-> fizzbuzzのため)

Elixirでは関数の引数に実際の値を記述しておくことで、その場合にのみ特定の関数を呼び出す事が可能であり
同名の関数を何個でも記述することが可能
(関数を定義するときはモジュール内部に記述する必要あり)

それぞれの対応する値段をパターンマッチを用いて返すという関数を作成した

defmodule Sample do
  def price("apple") do
    110
  end
  def price("banana") do
    150
  end
  def price("orange") do
    90
  end
  def price("grape") do
    540
  end
  def price(_fruit) do
    200
  end
end

in_cart = ["banana", "apple", "fish", "orange", "banana", "grape", "meat"]
convert_to_price = Enum.map(in_cart, fn item -> Sample.price(item) end)
IO.inspect(convert_to_price)

# [150, 110, 200, 90, 150, 540, 200]

まぁマップを使ってvalueを抜く方法は当然あるが、普通に書こうと思うとifをたくさん使うことになる
しかしElixirではパターンマッチを使って簡単に条件を書く事が可能であり、同名の関数を何個でも書けるので仕様変更にも強い
price関数にcherryを追加したければ、price("cherry")という関数を追加するだけだ

また受け取った値に対して、何らかの演算を行なってからパターンマッチを行たいときはガード節というものを使用する
例を見せた方が早い
映画の料金を年齢別に返す関数を作ってみた

defmodule Sample do
  def price(age) when age >= 20 do #20歳以上
    1500
  end
  def price(age) when age >= 10 do #10歳以上
    1000
  end
  def price(_age) do #10歳より下
    800
  end
end

Enum.map([25, 12, 9, 35, 4, 20, 10], fn age -> Sample.price(age) end)
|> IO.inspect()
# [1500, 1000, 800, 1500, 800, 1500, 1000]

パターンマッチは上から順にマッチされていくので2つ目のprice関数にマッチをする際には
1つ目の条件(age >= 20)が適用された状態なので、「20 > age >= 10」としなくても思ったように動く

以上の踏まえてElixirでfizzbuzzを作ってみる
rem()は除算の余りを求める関数である(eg: rem(30, 15) -> 0)

defmodule Sample do
  def fizz_buzz(num) when rem(num, 15) == 0 do
    "fizz_buzz: #{num}"
  end
  def fizz_buzz(num) when rem(num, 5) == 0 do
    "buzz: #{num}"
  end
  def fizz_buzz(num) when rem(num, 3) == 0 do
    "fizz #{num}"
  end
  def fizz_buzz(num) do
    "another #{num}"
  end
end

Enum.map(1..100, fn num -> Sample.fizz_buzz(num) end) |> IO.inspect()
# ["another 1", "another 2", "fizz 3", "another 4", "buzz: 5", "fizz 6",
# "another 7", "another 8", "fizz 9", "buzz: 10", "another 11", "fizz 12",
# "another 13", "another 14", "fizz_buzz: 15"....]

先ほども記述した通りに、仮に7の倍数にヒットさせたいとなった時は
同名のfizzbuzz関数を用意してrem(num, 7) == 0という条件を作成すれば良い
カスタムが非常に楽な上に強力。パターンマッチいいね

感想・まとめ

かなり駆け足で紹介したいものを紹介し尽くした
初めての試み故にうまく出来なかった部分もあるし、思ってたよりもウケた部分もあった
総じて反省、学習して次に生かせればと思う

最後に「Elixirめっちゃいいですね」と言ってもらえて良かった
また秋にでも入門会は開いてみようと思う

次回はfukuoka.exさんの開催するもくもく会にリモートで参加する予定です
また宜しくお願いします

fukuokaex.connpass.com

おまけのコーナー

さらに関数を省略形で記述することでfizzbuzzはもっと短くすることが出来る

defmodule ShortFizzbuzz do
  def fizz_buzz(num) when rem(num, 15) == 0, do: "fizzbuzz: #{num}"
  def fizz_buzz(num) when rem(num, 5) == 0, do: "buzz: #{num}"
  def fizz_buzz(num) when rem(num, 3) == 0, do: "fizz: #{num}"
  def fizz_buzz(num), do: "another: #{num}"
end

Enum.map(1..100, fn num -> ShortFizzbuzz.fizz_buzz(num) end)

いいね

【Elixirで学ぶCS】ElixirでアセンブラとVM変換器を実装するまで

なにこれ(5度目

Elixirでコンピューターサイエンスを学ぶシリーズの第5弾で以下記事の続編です

www.okb-shelf.work

この本を参考に勉強しつつコードを書いています

www.oreilly.co.jp

以前の記事まではかなり低レイヤーの部分をメインとして扱っていた
論理ゲートの実装から行い、加算器を作成し、ALUを作成し、メモリまでの実装が完了した

しかしながら、Elixir上でメモリをクロック同期がうんたらで実装させるには
参考にしている書籍では情報不足でHDL言語では何とか実装出来たのだが、Elixirで実装することが出来なかった
理由としては上の書籍ではCPUのクロック部分の実装は省略されており、付属のアプリケーションに委ねているためで
内部構成を上手く掴むことが出来なかったからだ

いずれはリベンジしたいとは思うが、ここで詰まって他の部分に触れないというのはもったいない
一旦、Elixirで進めるというチャレンジは停止していたが、6章から好きな言語を使って

を実装することが出来る
githubを除いてみたがElixirで実装している人は確認できなかったため、これはいいぞと思い
再び、Elixirのコードを書き始めた

現状としてはアセンブラ(一部バグあり)とVM変換器のメイン部分までの作成までを実装することが出来た(つまりは7章まで)
納品できるレベルではないが、完璧なコードを書くことを目的としておらず、まずはコンピューターサイエンスの全体像を掴むことをメインしているので
一旦は自分の中で良しとした。時間がある時にリベンジすれば良い

あと単純にアセンブラの文法に詳しくなりたいと全く思わないので突き詰める必要がないと判断した
話が少し長くなっているので、「そんなことは知っている」という方は読み飛ばして下さい

久々のブログでテンションが上がっている様です。ずっと仕事でしたので察して下さい

アセンブラについて

普段、我々が業務で使用している言語はいわゆる、高級言語と呼ばれるものであり
より人間が直感的に記述を行うことができるプログラミング言語である
しかしながら、今まで論理ゲートを実装してくる中で触れた通りに、実際にはコンピューターというのは0と1の情報だけで動作をしている

そのため、我々が汗水流しながら書いた高級言語を最終的には全てが0と1で記述されるバイナリコードのレベルまで翻訳をする必要がある
この翻訳の過程にはいくつかのステップが構成される

どうやらコンピューターによって使用が異なる場合がある様だが上から順に以下の様なレイヤー構造を持つ。名称は気にしなくて良い

ここで個人的に思ったことがある
論理ゲートの組み合わせが加算器になり、加算器などの組み合わせがALUになり、最終的にCPUになるという所からスタートしたが
このプログラミング言語が実行可能状態になるまで変換されるまでにも同じ様なレイヤーを持っていることに驚いた

コンピューターの設計の本質にあるのは「抽象化」であり、いつくのものレイヤーが重なることで複雑なことを可能としている
先人の知恵や努力が詰め込まれており、現代のコンピューターが動作をしている

また、この設計はそれぞれが独立したレイヤーとして作成されているため仕様変更も行いやすい
プログラミングでも同じことが言える。小出しに関数をレイヤー的に実装しておけば急な仕様の変更にも対応することが出来る
いいね

話はそれたが、アセンブラというものは書籍の内容と合わせて動きを追ってみる

@2
D=A
@3
D=D+A
@0
M=D

こんなような、かなり低レイヤーの言語をアセンブリ言語と呼ぶ。アセンブリアセンブラは異なるもので
アセンブリ言語バイナリーコードに変換するプログラムのことをアセンブラと呼ぶ

このアセンブリで書かれたコードをアセンブラを使ってバイナリーコードに変換するとこんな感じになる

0000000000000010
1110110000010000
0000000000000011
1110000010010000
0000000000000000
1110001100001000

もう何が何だが分からない。厳密には理解は可能だが可読性が悪いなんてレベルではない
アセンブリがどのように変換を行うかということに関しては説明をしない。書籍を読んだ方が早い
ざっくりとだけ自己満足程度に触れておくと、メモリ内に予め定義したコマンドの対応が用意されており(Elixirでいうmap(%{}))
その対応を元に1行ずつ解析を行なって変換していく

で、このアセンブリ言語を現代において書かれている方というのはあまり存在しないはず
先ほども話した様にコンピューターは構造的に設計されている。プログラミングが実行されるまでの過程でも同様であり
アセンブリ言語の上階層にはVM変換器」というものが存在している
VM変換器はコンパイラから受け取ったVM命令(スタック処理)をアセンブリに変換する

これが

# VM命令
push constant 7
push constant 8
add

こうじゃ

//push constant 7 
@7
D=A
@SP
A=M
M=D
@SP
M=M+1

//push constant 8
@8
D=A
@SP
A=M
M=D
@SP
M=M+1

//add 7 + 6
@SP
M=M-1
@SP
A=M
D=M
@SP
M=M-1
@SP
A=M
M=M+D
@SP
M=M+1

VM変換器を用いることで高級言語コンパイラされた結果に対してあらゆるプラットフォームで実行することができる
プログラミング言語を動作させる環境が変わっても、このVM変換器だけを対象のプラットフォームのために実装すれば良い
(メモリ内に定義されているコマンド対応がプラットフォーム毎に異なるため)

またVM変換器は複数の言語で使用することが可能であり、実行後の結果は同じアセンブリであるため
PythonからElixirを呼び出したりなんてことが可能になる

結局、何が言いたいのかというと、階層を分けて実装しておくことで仕様変更が楽に行えるということである
特にプラットフォームの互換性を保てるというのが凄い

Elixirでの実装

今回は上記のような変換を行うアセンブラVM変換器を実装した
厳密には機能が欠けているが理解に十分と判断したので手を止めた。全てのコードを解説しているとキリがないので
それぞれメイン関数の部分のみ順に説明をしておく。なお実装は書籍にある程度沿って実装した
コードがわりとボリューミィになったのでgithubを参照する
github.com

実装しての感想はElixirってやっぱりいいなって。
何がいいかというとコンピューターの設計は何回も述べている通り、抽象化に沿って階層的に設計されている
この階層的な処理は、流れをもっているという風に捉えることができて、まるでパイプ演算子のようだ
またElixirでは仕様変更、対応忘れてもうた〜、という時に同名関数のパターンマッチを楽々に追加することが出来る
Enum.map()の中でifやらcase書かなければいけないのは仕方がないがモジュール単位でこんなに仕様変更がしやすいのかと改めて驚いた
ifの条件を上から追っていく、つらい作業とはさよなら出来そうだ

アセンブラ

大まかな流れとしては

  • ファイルを読み込む
  • コマンドの種類の判定(AコマンドかCコマンド)
  • コマンドの領域を切り分け
  • バイナリコードへの変換

./assermbra.ex

defmodule Assembra do
  def to_binary(path, save_path) do
    """
      path -> 対象ファイルのパス
      save_path -> 対象ファイルの保存先のパス
    """
    # ファイルを読みこんでコメント記号、空白、改行記号を除去して文字列(各行)を配列にする
    res = Parser.read_file_and_adjustment(path)
          |> code_convert_to_command()
    
    # 変数の一覧表を作成する(symbol tabel)
    [symbol_table, _, _] = create_symbol_table(res)

    # 作成したシンボルテーブルを元に変数にメモリの番号を割り当てる
    replace_from_symbol_table(res, symbol_table)
    # コマンドを領域毎に分割
    |> Enum.map(&(Parser.symbol_or_mnemonic(&1)))
    # 領域毎に分割したコマンドをバイナリーに変換
    |> code_to_binary()
    |> save_to_hack_file(save_path) #保存
  end
end

VM変換器

大まかな流れとしては

  • ファイルを読み込む
  • コマンドの種類の判定(何のコマンドか)
  • コマンドから引数を抜き出す(引数は2で固定なため1,2)
  • VM命令(スタックコマンド)からアセンブリへの変換

./VMtranslator.ex

defmodule VMtranslator do
  def main(path) do
    # 絶対pathからファイル名と保存先を切り出し
    save_path = String.slice(path, 0..-3) <> "asm"
    file_name = String.split(path, "/") |> Enum.at(-1) |> String.slice(0..-4)
    save_string = Parser.read_file_and_adjustment(path) #ファイル読み込みと整形
      |> Stream.map(&(Parser.command_type(&1))) #コマンド種類の判定
      |> Stream.map(&(Parser.arg1(&1))) #第1引数を抜き出し
      |> Stream.map(&(Parser.arg2(&1))) #第2引数を抜き出し
      |> Enum.to_list() #stream -> listに 
      #ファイルの行数をaccumlatorに保存しつつ、mapを実行してアセンブリに変換していく
      |> Enum.map_reduce(0, fn {command, arithmetic, arg1, arg2}, acc ->
          if arithmetic in ["eq", "gt", "lt"] do
             # 特定条件の時にaccに+1をする(変数名が重複しないように)
            {CodeWriter.write_arithmetic({command, arithmetic, arg1, arg2, acc}), acc+1}
          else
            #staticの時にはファイル名を渡す
            if arg1 == "static" do
              {CodeWriter.write_arithmetic({command, arithmetic, arg1, arg2, file_name}), acc}
            else
              {CodeWriter.write_arithmetic({command, arithmetic, arg1, arg2, acc}), acc}
            end
          end
        end)
      # タプルから抜き出して保存
      |> (fn {res, _} -> res end).()
      |> Enum.map(fn {_, converted} -> converted end)
      |> Enum.join("\n")
    File.write(save_path, save_string)
  end
end

かなりざっくりだが、こんな感じになった
今まで作成したコードも合わせて一度githubにpushした

先に本書の8章~13章までの内容には実装をせずに目を通した
ここに来るまでに

という部分までを最低限触れたつもりだ
CPUの実装がElixirで出来なかったことはやや残念だが時間も有限なので仕方ない

本書をベースにすると今後、残されたテーマがコンパイラとOSだが
それぞれの解説が分かりにくかったため、本書を使用するのはここで終わろうと思う

コンパイラに関しては良い記事を発見しているので、そちらを参照していく
OSに関しても別書籍で1冊、目ぼしいものを確認しているのでそちらを