はじめに
ストックマーク株式会社のエンジニア @nyancat3 です。 先日RubyKaigi 2024に一般参加しました。印象に残った8セッションの概要と感想を書きます。
Day 1
- Writing Weird Code
- The grand strategy of Ruby Parser
- Strings! Interpolation, Optimisation & Bugs
- Let's use LLMs from Ruby 〜 Refine RBS types using LLM 〜
Day 2
- Finding Memory Leaks in the Ruby Ecosystem
- Breaking the Ruby Performance Barrier
- Squeezing Unicode Names into Ruby Regular Expressions
Day 3
- Speeding up Instance Variables with Red-Black Trees
Day 1
Writing Weird Code
スライド:
タイトル通り、奇妙なRubyコードが沢山紹介されました。 実行するとアニメーションになるRubyコードや、.bmpで一見画像に見えるけれど実は実行できるRubyコード、短くて奇妙なコードなどが紹介されました。
Animated Ruby Quines in #rubykaigi keynote from @tompng 🤯 pic.twitter.com/uVA2LM13eX
— Mike McQuaid (@MikeMcQuaid) May 15, 2024
アニメーションはQuine (自身のソースコード自体を出力するプログラム) で書かれています。正直仕組みはほぼ理解できていないですが (GitHub Copilotに説明させようとしてみましたがもちろん無理でした)、sin, cos, 複素数によって回転する動きを実現したり、 Time.now
を上書きすることで実現しているようです。Rubyはモンキーパッチしやすい言語、とおっしゃっていました。
各コードは発表者のtompngさんが公開しているので、ぜひ手元で実行してみてください。数十行のRubyコードを実行するだけで、こんなに複雑なアニメーションを実現できるんだ…と驚きます。例えばkurage.rbは、クリックするとその位置を避けて動きます。
ちなみに BEGIN
というblockを書くと、そこからRubyコードは実行される、ということを初めて知りました。
奇妙なコードを普段から書くことで、OSSにコミットするときに奇妙な挙動やコーナーケースに気づけたり、言語の理解が深まる、とのことでした。
The grand strategy of Ruby Parser
スライド:
当日はParser自体が何なのか?すら理解していなかったので難しかったのですが、復習してみたら発表者の金子さんの記事や昨年のスライドがわかりやすかったです。
ざっくり表現すると、RubyのParserは、Rubyのスクリプトを解析して、AST (Abstract Syntax Tree: 抽象構文木) に変換するものです。
Rubyの表現力や自由度は高いですが、それゆえに字句解析や構文解析は大変なのだな、と分かりました。
例えば ||
はORとして使われることが一般的ですが、Rubyだと省略されたブロック引数としても使えます。
10.times do ||
end
lexer (字句解析器) は通常ステートレスですが、このような判定には lex_state
というステートが必要になるとのこと。しかしできれば状態管理はParserにさせたくないとのことでした。
C言語で実装したParserを色々な言語で使っているが、本当はその言語用のParserはその言語で書くべきではないか (RubyのParserならRubyで書くべきではないか)、という考えがあるそうです。Ruby 3.4ではUniversal Parserを完成させたい、とのことでした。
Strings! Interpolation, Optimisation & Bugs
8行削除しただけでstringクラスの処理を2倍高速化できた、という内容でした。RubyKaigiのセッションでは珍しく、抽象的な教訓にまでに落とし込んだプレゼンだったので、普段の開発に活かせそうな考え方を知れて面白かったです。
実際のPRと該当コードはこちら。
マジックナンバーとして条件分岐に使われていた MIN_PRE_ALLOC_SIZE 48
という数字は、書かれた当時のマシンスペックをカバーするためのものだと判明しました。今はもう必要ないと分かったので、シンプルにマジックナンバーと条件分岐を削除したら、それだけで処理が2倍高速化したそうです。
#define MIN_PRE_ALLOC_SIZE 48
// ...
if (LIKELY(len < MIN_PRE_ALLOC_SIZE)) {
str = rb_str_resurrect(strary[0]);
s = 1;
}
else {
str = rb_str_buf_new(len);
rb_enc_copy(str, strary[0]);
s = 0;
}
→
str = rb_str_buf_new(len);
str_enc_copy_direct(str, strary[0]);
教訓として "小さい変更が大きな成果になり得る" と同時に、裏返せば "どんな変更でも予期せぬ影響になり得る" とおっしゃっていました。 また "常に思い込みを疑うべき"、 "多様性のある考え方で問題を解決できることがある" ともおっしゃっていました。
Let's use LLMs from Ruby 〜 Refine RBS types using LLM 〜
スライド: slides.com
rbs-gooseというツールを開発して、LLMでRBSの型推測を試みた、という内容でした。
この日の前日5月14日にちょうどOpenAI GPT-4oが発表されていました。他社モデルも含めて各LLMでRBS生成結果を比較したら、OpenAI GPT-4oは最速なのに理想的な出力で、更にコストも安く、非常に優秀な結果だったとのことでした。本題から少し逸れますが、GPT-4oを今すぐ使わなければ!という気持ちになりました。
Day 2の発表 "Embedding it into Ruby Code" のrbs-inlineを使って、インラインコメントとして型を書くようになったら、GitHub Copilotが補完を上手くしてくれそうなので、今回作ったRBS gooseはもしかしたらいらなくなるかも…でももう少し開発を続けてみたい、とのことでした。
Day 2
Finding Memory Leaks in the Ruby Ecosystem
スライド: rubykaigi_2024_slides
内容は以下で、とてもC言語なセッションでした。実際のIssueはこちら。
- Ruby自体のメモリリークを防ぐために、終了時にメモリを解放する
RUBY_FREE_AT_EXIT
という環境変数をRuby 3.3で追加した - その環境変数
RUBY_FREE_AT_EXIT
を有効にすることで、native gem開発者用のメモリリーク検出gem ruby_memcheckで、natvie gem自体のメモリリークを効果的に検出できるようになった- nokogiri, liquid-c, protobuf, gRPC などのメモリリークを実際に検出した
native gemのコントリビューターなら RUBY_FREE_AT_EXIT
を使ってメモリリークを見つけてね!とのことでした。
メモリリークの例として挙げられたCコードは以下です。ループ内でメモリを割り当て続けます。
int main() {
while(1) {
char * name = malloc(256); // Allocate in Loop
if(fgets(name, 256, stdin) == NULL) {
break;
}
printf("%s", name);
}
}
これを以下に修正すれば、ループ外のメモリ割り当て & メモリ解放になり、メモリリークを防げます。
int main() {
char * name = malloc(256); // Allocate once
while(1) {
if(fgets(name, 256, stdin) == NULL) {
break;
}
printf("%s", name);
}
free(name); // Free before exit
}
gem ruby_memcheckでは、メモリリーク検出にValgrindという検出ツールを使っています。ただRuby自体のリークが多く検出されるため、native gem自体のメモリリークはどれか?が分かり辛かったそうです。
そこでRuby 3.3に RUBY_FREE_AT_EXIT
環境変数を導入して、 export RUBY_FREE_AT_EXIT=1
で有効にすれば、Rubyはプログラム終了時にメモリを解放するようになりました。先ほどのCコードの例で言うと free(name); // Free before exit
を行うイメージです。
これによりRuby自体のメモリリークが大幅に減少して、native gem自体のメモリリークはどれか?の特定が簡単になりました。
Breaking the Ruby Performance Barrier
YJITの登場によりRubyの処理が高速化した、というのは他のセッションでも触れられていて周知の事実です。このセッションは、YJITでRubyが高速化したことにより、今までC言語で書かれがちだったgemもRubyで書いていける、という内容でした。
Rubyで実装されたgem (pure-Ruby gem) の例として、以下が挙げられていました。
- redis-rb/redis-client: Simple low level client for Redis 6+
- tenderlove/tinygql: A tiny and experimental GraphQL parser in Ruby
- rmosolgo/graphql-ruby: Ruby implementation of GraphQL
- ruby-protobuf/protobuf: A pure ruby implementation of Google's Protocol Buffers
例えばRubyで書かれたprotobufはまだ実験段階のgemだそうですが、Cで書かれたGoogle本家のprotobufより9倍も速いそうです。YJITが入る前は7倍遅かったそうです。YJITすごい。
Rubyでgemを実装する良さとして、YJITでCに近いパフォーマンスが出せることはもちろん、便利なstring methodsが多いことも挙げられていました。パフォーマンス重視のgemはCで実装しなければ、という常識が変わった、とのことでした。
Squeezing Unicode Names into Ruby Regular Expressions
Unicodeの名前 (Unicode Name Property) をそのままRuby正規表現に使えないか?そうするときにどうやってメモリを節約するか?という内容でした。
Unicodeの名前 (Unicode Name Property) というのは、例えば絵文字 🤬
は SERIOUS FACE WITH SYMBOLS COVERING MOUTH
、ひらがな あ
は HIRAGANA LETTER A
という名前でUnicode登録されていて、その直感的に理解できる文字列のことを指します。
現状Rubyでは、例えば /あ/
や /\u3042/
(あ
のUnicodeは U+3042
) は有効な正規表現です。
/あ/ =~ "とらいあすろん"
=> 3
/\u3042/ =~ "とらいあすろん"
=> 3
しかし /HIRAGANA LETTER A/
と書くことはできません。これを実現するには HIRAGANA LETTER A
という文字列を U+3042
に何とか変換する必要があります。Unicodeの文字種類は多いですし、Unicodeの名前はとても長いこともある (80字以上のものもある) ので、メモリをうまく節約したいです。
そこでTrieを使います。例えば、 SERIOUS FACE WITH SYMBOLS COVERING MOUTH 🤬
に1番近いUnicodeの名前は SERVICE MARK ℠
で、近い5個の文字を1つのエントリーにまとめることでメモリを節約するそうです。Twitterで "百人一首の決まり字" と表現している方がいて上手いと思ったのですが、 SER
の次の文字が決まれば名前を特定することができます。
またTrieの葉から、sparse nodes (子を持たないノード) を減らすことで、全bitを有効に使うアプローチなども紹介されていました。
Day 3
Speeding up Instance Variables with Red-Black Trees
Ruby 3.4で、インスタンス変数へのアクセスの高速化を、Red-Black Trees (赤黒木) を用いたキャッシュで行った、という内容でした。実際のPRはこちら。 github.com
赤黒木をそもそも知らなかったのですが、このYoutube動画が分かりやすかったです。4分で学べます。 youtu.be
赤黒木は二分探索木の一種で、各ノードに赤または黒の色が付いています。以下のような条件があり、それによりself balancing (自己平衡) が実現されます。木のバランスが保てているため、検索が簡単という良さがあります。
- 赤ノードは赤の子を持たない。黒の子のみ持つ
- 葉ノード(末端のノード)はいつも黒
- 任意のノードからその子孫の葉ノード(末端のノード)までの全パスで、黒ノードの数は同じ
Ruby 3.4では、インスタンス変数が既に定義されているか?を赤黒木を用いたキャッシュで確認していて、これによりキャッシュミスの場合のパフォーマンスが良くなるそうです。
- Ruby 3.2: インスタンス変数にキャッシュがそもそも使われていなかった
- Ruby 3.3: キャッシュが使われるようになり、キャッシュヒットの場合のパフォーマンスが良くなった。ただ赤黒木を使えていなかったので、キャッシュミスの場合のパフォーマンスはあまり改善しなかった
- Ruby 3.4: 赤黒木が使われるようになり、キャッシュミスの場合のパフォーマンスも良くなった
だからRuby 3.4は速いですよ!とのことでした。2024年5月28日現在、まだRuby 3.4.0はpreviewなので正式リリースが楽しみです。
おわりに
RubyKaigiはRubyの言語の内部仕様が主な話題で、Matzも言及していた通りRubyKaigiというよりCKaigiなので、セッションはとても難しかったです。ただ初参加でも、復習すればこの記事のように1%くらいは理解できましたので、気になる方は参加してみると良いと思います。
Stockmarkでは一緒にプロダクトと組織を成長させていただける方を広く募集しています。 カジュアル面談からお気軽にご連絡ください。