trick2015 ko1さんの1つ目のやつを読むやつ

前回に引き続き trick コードを読んだのを書きます、といっても実はこっちのほうが先だったのですが。最近これに関連するものを twitter で教えてもらったので書きます。

前回はこちら

hkdnet.hatenablog.com

trick とは

なんかよくわかんないけどすごい ruby コードのコンテストです。すごい ruby コードは ruby に対する深い理解に基づいて書かれていることが多く、読むだけで勉強になったりならなかったりします。

ko1_1

バック・トゥ・ザ・フューチャーネタのほうです。

https://github.com/tric/trick2015/blob/master/ko1_1/entry.rb

コアコンセプトはそんなに難しくなくて、すべてのオブジェクトにアクセスできる ObjectSpace から iso8601 形式で書かれた時刻を昇順で取り出して、unixtime化して 0xff論理積をとるといい感じにコードポイントに落ちてメッセージが表示できる、という感じです。1

一応小ネタを解説しておくと。 Integer#chr は自身をコードポイントとして見たときに対応する文字を1つ返すやつです。対応表は ascii 対応表とかを見るとよいと思います。

97.chr # => 'a' 
'a'.ord # => 97

最終的な出力は eval によって行われています。evalp に変えて実行してみると、ローカル変数 s をごにょごにょした結果は最終的には puts '"Nope. Already been there."' という文字列になり、これを eval して出力してることがわかります。

だけども

iso8601形式の文字列たちが実際の実行コードを作っていることはわかりました。しかし、 ObjectSpace.each_object{|u|s<<u} でその時刻文字列を拾ってこれるのはなんで?という疑問はあります。

例えば以下のコードだとローカル変数 f が保持しているインスタンスを見れるのは妥当な気がします。

class Foo
end

f = Foo.new
p ObjectSpace.each_object { |e| break e if e.is_a?(Foo) } # => #<Foo:0x00007fb5c98fdd70>

一方で今回のコードでは 0.times { } 内で宣言されてる %w リテラルです。これは 0.times なのでランタイムでは実際にオブジェクトを生成する必要はないですし、作成してても GC.start したら消えそうな気もします。

と、ここで RubyKaigi2018 で tenderlove の発表を思い出すと、Rubyソースコードを解釈したあとASTを作成し、VMに実行させるための命令列を作成すること、またその命令列は Ruby 上のオブジェクトであるという話でした。2 つまり、実行していなくても生成した iseq (命令列) の中に文字列リテラルがいるような気がしてきます。僕の理解が正しければあの発表の後半は、そういった iseq 上の Ruby オブジェクトをGCされないように mark array を使っているがそれのメモリ使用量を減らすために〜という話だったはず。

というわけで iseq が掴んでるんじゃないかという推測をしていたのですがどうやって確かめたらいいのかよくわかりません……。iseqが掴んでいるかどうか……?そんなんわかるの……?と思っていたら twitter で教えてもらいました。

↑フクロウとお化けのリプライにカエルが恐縮する図

たしかに --dump=insns で iseq が文字列リテラルを掴んでそうな様子が確認できます。

$ ruby --dump=insns ko1_1/entry.rb
== disasm: #<ISeq:<main>@ko1_1/entry.rb:1 (1,0)-(46,54)> (catch: TRUE)
== catch table
| catch type: break  st: 0000 ed: 0005 sp: 0000 cont: 0005
| == disasm: #<ISeq:block in <main>@ko1_1/entry.rb:1 (1,7)-(39,2)> (catch: FALSE)
| == catch table
| | catch type: redo   st: 0001 ed: 0079 sp: 0000 cont: 0001
| | catch type: next   st: 0001 ed: 0079 sp: 0000 cont: 0079
| |------------------------------------------------------------------------
| 0000 nop                                                              (   1)[Bc]
| 0001 putstring                    "2422-02-10T21:45:38+09:00"         (   2)[Li]
| 0003 putstring                    "2580-06-19T08:53:09+09:00"         (   3)
| 0005 putstring                    "2233-01-20T02:06:42+09:00"         (   4)

また謎 patch により current_iseq というメソッドが生えて iseq にアクセスして object_id をとることができるようになって、確かに object_id まで一致していることが確認できました。まだ謎 patch の中身が完全に謎なのでそのへんを追っていきます。疲れたのでまた次回。


  1. このくらいなら割と素直に読めると思ってしまうが(突然のイキり)読書会参加者の雰囲気を見るとそうでもないのかもしれない

  2. このへんから https://speakerdeck.com/tenderlove/reducing-memory-usage-in-ruby?slide=102