LoginSignup
10
0

More than 3 years have passed since last update.

Rubyのシンボルがオレの思っていた挙動と違う

Last updated at Posted at 2019-12-11

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対象だったら困ったことになりませんかと。

a.rb
"hogehoge".to_sym
require "./b.rb"
b.rb
:hogehoge
$ ruby a.rb

さて、上記のようなプログラムはどうなりますか・・・?a.rbではhogehogeは動的シンボルであり、b.rbではhogehogeは静的なシンボルになります。
さて、この両方のhogehogeは違うオブジェクトなのでしょうか?

a.rb
puts "hogehoge".to_sym.object_id.to_s(16)
require "./b.rb"
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対象にしています。

symbol.c
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対象のハッシュにセットしています。

symbol.c
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関数が呼ばれると。。。

symbol.c
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 をご覧ください。

そのほかの職種は、エイチームグループ採用サイトをご覧ください。

10
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
0