Ateam cyma Adevent Calendar 2019、12日目です!
本日は株式会社エイチームでcymaのエンジニアの @bayasist が務めさせていただきます。
cymaではリプレイス後にRubyを使い始めました。Rubyを初めて触ったときにシンボルという概念に違和感を覚えました。ということでシンボルについて違和感がなくなるまで深ぼってみることにします。
Symbolと文字列
rubyのSymbolはよく文字列と比較されます。文字列との違いを簡単に説明します。
いろんな方が記事をあげてらっしゃるので簡単に流します。
表記
- 文字列・・・
"String"
- Symbol・・・
:Symbol
シンボルは上記のようにコロンからスタートします。
メモリの使い方が違う
シンボルは文字列と違い、同一の内容であれば同一のオブジェクトを使用します。文字列は同一の内容であっても同一のオブジェクトである保証はありません。(メモリ上に同じ文字列が複数展開されていて、それぞれを使っていたり)
Rubyでは同一のオブジェクトであればobject_idが等しくなるので、それを利用して簡単に検証してみましょう。
"Hello Qiita".object_id == "Hello World".gsub("World", "Qiita").object_id
"Hello Qiita".to_sym.object_id == "Hello World".gsub("World", "Qiita").to_sym.object_id
実行結果
false
true
gsubは第一引数の文字を第二引数の文字に置き換える
to_symは文字列をSymbolに変換する
右辺も左辺も"Hello Qiita"ですが、文字列の方はオブジェクトが異なるためfalseとなりました。
"Hello Qiita"という文字がメモリ上2か所に存在し、それぞれのobject_idを比較しているからです。
一方でSymbolに変換してからobject_idを比較すると必ず同一になります。二つのシンボルが同一のオブジェクトであることが分かると思います。
使われる用途が違う
上記から分かるようにSymbolはobject_idさえ比較してしまえば内容が同一かどうかが判断可能です。ですので一致しているか否かを比較的早く判断することができます。
このような特性からハッシュのキーによく利用されます。また、Ruby内部の処理としてメソッド名などにも用いられています。
シンボルの大量生成によるメモリの枯渇?
さて、ここまではRubyの基礎知識として出てくる話ですが、これを聞いて私は「???」となったわけです。
筆者の頭の中では「シンボルのobject_idと文字列の対応表が多分メモリ空間上のどこかに存在して、その対応表を見ながらデータを作成しているんだろう。あれ、ということは大量にシンボルを作りすぎるとメモリが枯渇する・・・・?」
当時ここまではっきりと思ったかどうか忘れましたが、なんとなくこの辺でモヤっとしていたのは覚えています。そして大量にシンボルを作りすぎるとメモリが枯渇するは半分正解。半分不正解。Ruby 2.2で改良されるまでは、メモリリークの危険性がありました
Ruby 2.2での改良
Ruby 2.2でシンボルの種類を静的シンボルと動的シンボルに分けるように仕様変更がされています。(動的シンボル、静的シンボルはC言語のソースコード上でDYNAMIC_SYM、STATIC_SYMとあったのでこう書きましたが、日本語名が違っていたらご指摘ください。)
因みに静的シンボルはプログラム上に直接書かれたシンボル、動的シンボルはプログラムの実行によってつくられたシンボルです。
# 静的シンボル
:a
# 動的シンボル
"a".to_sym
静的シンボルは従来通りの挙動をしますが、動的シンボルは使っていないものをGCで回収します。これを見た瞬間はマジか。。。と思いました。
なぜマジか。。。と思ったかは次の章で。
本当に同一の文字の時に同一のオブジェクト?
動的シンボルのオブジェクトが使われていなければGCで回収されるということは、同一の文字なら同一のオブジェクトって担保されないのでは?と思いました。だって対応表を破棄しちゃうんでしょ・・・?
ということで、実験です。
1から1,000,000までのシンボルを2回作り、それぞれ10のシンボルのobject_idを表示します。
(1..1000000).each do |i|
sym = i.to_s.to_sym
puts sym.object_id.to_s(16) if i == 10
end
(1..1000000).each do |i|
sym = i.to_s.to_sym
puts sym.object_id.to_s(16) if i == 10
end
実行結果
18583dc
1765d44
うぉぉぉぉぉ。まじかぁぁぁぁぁ!
ちなみに、ちゃんと変数に格納するなど利用をされていればGCによって回収されることはないため、上記のような事態は起きません。
arr = []
(1..1000000).each do |i|
sym = i.to_s.to_sym
arr << sym
puts sym.object_id.to_s(16) if i == 10
end
(1..1000000).each do |i|
sym = i.to_s.to_sym
puts sym.object_id.to_s(16) if i == 10
end
安心安全の実行結果
18a0394
18a0394
シンボルは同一内容だからと言って同一のオブジェクトとは限らない
ということで上記の実験から、同一内容だからと言って同一オブジェクトとは限らないと分かりました。とはいえ、利用され続けている限りにおいては、GCによって回収されないことから、同一オブジェクトであることは保証されます。ですので、よっぽどこのことでハマることは無いかと思います。
ただ、シンボルソノモノではなく、シンボルのobject_idをアレコレしだすとハマるかもしれません。シンボルのobject_idのみを変数に保存したりですね。そういうことはやめましょう。
疑問解決!
え?本当に???
え?本当に動的シンボルはGCされるんですか・・・?
ここでふと疑問が。。。上記で説明した動的シンボルがGC対象だったら困ったことになりませんかと。
"hogehoge".to_sym
require "./b.rb"
:hogehoge
$ ruby a.rb
さて、上記のようなプログラムはどうなりますか・・・?a.rbではhogehogeは動的シンボルであり、b.rbではhogehogeは静的なシンボルになります。
さて、この両方のhogehogeは違うオブジェクトなのでしょうか?
puts "hogehoge".to_sym.object_id.to_s(16)
require "./b.rb"
puts :hogehoge.object_id.to_s(16)
$ ruby a.rb
実行結果
18ae6c4
18ae6c4
そんなわけないですよね。ただ、この場合先に"hogehoge".to_symを実行しているため、hogehogeは動的シンボルとなります。「えーーーーじゃあGCされちゃうの?シンボルってメソッド名としてもつかってるんだよね?GCされちゃったらメソッド呼び出しできないじゃん!」という心配があるわけです。
一回メソッド名などに使うとGC対象から外れる
はい、と上記の通り、メソッド名などに一回以上シンボルを使えば元が動的シンボルであってもGC対象から外れます。
本当に?と思ったので、この辺はC言語の実装を確認。
Symbolの初期化時にglobal_symbols.dsymbol_fstr_hashをGC対象にしています。
void
Init_sym(void)
{
VALUE dsym_fstrs = rb_ident_hash_new();
global_symbols.dsymbol_fstr_hash = dsym_fstrs;
rb_gc_register_mark_object(dsym_fstrs);
(以下略)
}
Symbol作成時に先ほどのGC対象のハッシュにセットしています。
static VALUE
dsymbol_alloc(const VALUE klass, const VALUE str, rb_encoding * const enc, const ID type)
{
(略)
rb_hash_aset(global_symbols.dsymbol_fstr_hash, str, Qtrue);
RUBY_DTRACE_CREATE_HOOK(SYMBOL, RSTRING_PTR(RSYMBOL(dsym)->fstr));
return dsym;
}
そして、rb_sym2id関数が呼ばれると。。。
ID
rb_sym2id(VALUE sym)
{
ID id;
if (STATIC_SYM_P(sym)) {
id = STATIC_SYM2ID(sym);
}
else if (DYNAMIC_SYM_P(sym)) {
(略)
rb_hash_delete_entry(global_symbols.dsymbol_fstr_hash, fstr);
(以下略)
}
おお、確かにGC対象から外されておる!
(ソース追っていけばわかりますが、メソッド名のシンボルはrb_sym2idを通過します)
ということで、疑問解決です!
最後に
Ateam cyma Adevent Calendar 2019 の 12日目、いかがでしたか。
13日目はcymaの飲み会のあとはパフェを食べる系エンジニア @ihsiek さんが前職の失敗談を書いてくれるそうです。
株式会社エイチームでは、一緒に働けるチャレンジ精神旺盛な仲間を募集しています。
エンジニアとしての働き方に興味を持たれた方はcymaの Qiita Jobs をご覧ください。
そのほかの職種は、エイチームグループ採用サイトをご覧ください。