はじめに
こんにちは、LANSCOPE セキュリティオーディター開発チームの東(あずま)です。
今回は、開発で使用しているプログラミング言語ElixirのAtomとメモリ最適化について学んだ内容をご紹介します。
Elixirの「Atom」とは
Elixirでは「:ok」や「:error」といった形でコロンから始まる「Atom」と呼ばれる不変定数があります。よく使われるケースとしては、下記のような実行結果のパターンマッチングを行う場合に使用します。
{:ok, date} = Date.new(2023, 12, 31) # OK {:error, reason} = Date.new(2023, 13, 1) # NG
このAtomを見ない日は無いといっても過言ではないくらい頻出します。しかし、自分はパターンマッチングで使用する便利機能という認識のみで、仕様理解が出来ているとは言えない状態でした。
そこで、正しく理解するためにも公式ドキュメントの調査を行いました。
「Atom」の仕様
ElixirはErlangベースのため、Erlangの言語仕様ドキュメントを調査しました。その結果、下記の記載があることに気付きました。
An atom refers into an atom table, which also consumes memory. The atom text is stored once for each unique atom in this table. The atom table is not garbage-collected.
(AtomはAtomテーブルを参照し、テーブル自身もメモリを消費する。Atomテキストは、テーブル内の一意のAtom毎に一度だけ保存される。Atomテーブルは、ガベージ・コレクションされない。)
https://www.erlang.org/doc/efficiency_guide/advanced, 訳は引用者による
「Atomテーブルは、ガベージ・コレクションされない」と記載されています。
ガベージ・コレクションとは、不要になったメモリ領域を自動的に解放してくれる機能です。
この機能が無いということは、「Atomテーブルは永続的にメモリを占有している」ということになります。
「Atom」とメモリ最適化
そこで実際にプログラミング言語が実装されているソースコードを見てみました。 Elixirでは「String.to_atom/1」で文字列からAtomに変換できますが…
## https://github.com/elixir-lang/elixir/blob/v1.16.0/lib/elixir/lib/string.ex#L2736 def to_atom(string) when is_binary(string) do :erlang.binary_to_atom(string, :utf8) end
Erlangの「binary_to_atom」を呼び出すため、既に存在するAtomでもmake_atom(uix)が呼ばれてしまい、新たに作成されてしまいます。
## https://github.com/erlang/otp/blob/OTP-26.2.1/erts/emulator/beam/erl_unicode.c#L2056 BIF_RETTYPE binary_to_atom_2(BIF_ALIST_2) { return binary_to_atom(BIF_P, BIF_ARG_1, BIF_ARG_2, 0); } ## 第四引数のmust_existが0(false)のため、既に存在するAtomでもmake_atom(uix)が呼ばれてしまう ## https://github.com/erlang/otp/blob/OTP-26.2.1/erts/emulator/beam/erl_unicode.c#L1998 static BIF_RETTYPE binary_to_atom(Process* proc, Eterm bin, Eterm enc, int must_exist) { byte* bytes; byte *temp_alloc = NULL; Uint bin_size; Eterm a; .... if (!must_exist) { .... a = make_atom(uix); } else if (!erts_atom_get((char*)bytes, bin_size, &a, ERTS_ATOM_ENC_UTF8)) { goto badarg; } ....
そのため、一度作成したものはガベージ・コレクションされないことを考慮し、既存のものを流用したいです。
そこで、Elixirの「String.to_existing_atom/1」を呼ぶと...
## https://github.com/elixir-lang/elixir/blob/v1.16.0/lib/elixir/lib/string.ex#L2768 def to_existing_atom(string) when is_binary(string) do :erlang.binary_to_existing_atom(string, :utf8) end
Erlangの「binary_to_existing_atom」が呼び出されるため、erts_atom_getによって既に存在するものが返されます。
これにより、新たにAtomが作成されることを防げます。
## https://github.com/erlang/otp/blob/OTP-26.2.1/erts/emulator/beam/erl_unicode.c#L2061 BIF_RETTYPE binary_to_existing_atom_2(BIF_ALIST_2) { return binary_to_atom(BIF_P, BIF_ARG_1, BIF_ARG_2, 1); } ## 第四引数のmust_existが1(true)のため、erts_atom_getが呼ばれる ## https://github.com/erlang/otp/blob/OTP-26.2.1/erts/emulator/beam/erl_unicode.c#L1998 static BIF_RETTYPE binary_to_atom(Process* proc, Eterm bin, Eterm enc, int must_exist) { byte* bytes; byte *temp_alloc = NULL; Uint bin_size; Eterm a; .... if (!must_exist) { .... a = make_atom(uix); } else if (!erts_atom_get((char*)bytes, bin_size, &a, ERTS_ATOM_ENC_UTF8)) { goto badarg; } ....
事前にAtomが定義されていないとエラーになるため注意が必要ですが、使いまわせる場合は使用することで最適化できると考えられます。
おわりに
今回はElixirのAtomについて、プログラミング言語の実装部分まで掘り下げていきました。 この経験を経て、公式ドキュメントやプログラミング言語を実装しているC言語ソースコードを読むことで、理解を深めることができました。
普段から使用している関数であっても、改めて調べた上で工夫を行うことで、効率的な実装に繋がると思います。
ここまでお読みいただき、誠にありがとうございます。 本内容がお役に立てれば幸いです。