簡潔ビットベクトルでRubyをlog N倍速くした

技術部のフルタイムRubyコミッタの遠藤(@mametter)です。昨日の Hackarade #04 の開催報告に続き、2日連続で記事を投稿します。

今回は、ある条件下でのRubyの実行速度を高速化した話を紹介します。この改善はすでにMRIの先端にコミットされていて*1、年末リリース予定のRuby 2.6に含まれる予定です。

ひとことで言うと、「簡潔ビットベクトルを索引に使うことで、プログラムカウンタから行番号を計算するアルゴリズムをO(log N)からO(1)に改善した。これにより、TracePoint有効時やコードカバレッジ測定下で、長さ N のメソッドの実行が O(N log N) から O(N) に高速化される」ということです。順に説明します。

背景:Rubyのバイトコードの構造

この最適化を理解するにはまず、Rubyのバイトコードのある特徴を知る必要があります。

たとえば

x = :foo
y = :bar
z = :baz

というRubyプログラムは、概念的には次のようなバイトコードになります。

PC 命令 行番号
0000 putobject :foo 1
0002 setlocal x
0004 putobject :bar 2
0006 setlocal y
0008 putobject :baz 3
0010 setlocal z

PCはプログラムカウンタ、putobjectはオブジェクトをスタックにpushする命令、setlocalはスタックのトップを指定変数に代入する命令です。ソースコードの行番号を保持しているのがポイントです。行番号の情報は、例外発生時のバックトレースや、コードカバレッジの測定などで必要になります。

ここで注目してほしいのは、行番号の情報は一部の命令だけが持っているということです(他に、TracePointのイベントタイプも、一部の命令だけが保持します)。そのため、このバイトコードをそのままメモリに保持すると、無駄が生じることになります*2。そこで、RubyのVMはこのテーブルを「命令列」と「命令情報テーブル」という2つのテーブルに分解して保持しています。

PC 命令
0000 putobject :foo
0002 setlocal x
0004 putobject :bar
0006 setlocal y
0008 putobject :baz
0010 setlocal z

図↑:命令列

図↓:命令情報テーブル

PC 行番号
0000 1
0004 2
0008 3

命令情報テーブルは、行番号のある命令の分しか情報を保持しないので、余計なメモリを消費しません。

性能の問題

メモリ消費量を減らすことはできましたが、実際に行番号を調べる必要が生じたとき、命令情報テーブルを表引きする必要があります。この表引きは、Ruby 2.4は線形探索でO(N)、Ruby 2.5は二分探索に改善してO(log N)でした。

この表引きが定数時間でないために、行番号やTracePointイベントフラグを頻繁に参照するような状況下(たとえばTracePoint有効下)で、ちょっとびっくりする性能特性が生まれます。次の図は、ローカル変数への代入をN回やるだけのN行のメソッドを、TracePoint有効下で実行するのにかかる時間を測定した結果です。

N行のメソッドの実行にかかる時間が非線形になっていることがわかります。10000行もあるようなメソッドを書くことは(自動生成を除けば)稀なので、あまり問題になっていませんでしたが、直観的な挙動とは言えません。このようになるのは、N行のメソッドはO(N)個の命令からなり、線形探索(Ruby 2.4)だと命令ごとにO(N)の表引きが行われるので、メソッドの実行全体ではO(N ^ 2)の時間がかかるためです。二分探索(Ruby 2.5)は圧倒的に改善していますが、まだO(N log N)で、線形にはなっていません。

今回は、さらなる最適化として、命令情報テーブルに「簡潔ビットベクトル」を索引としてもたせることで、表引きを定数時間に改善しました。これにより、N行のメソッドの実行にかかる時間がO(N)という、当たり前の期待を達成することができます。

簡潔ビットベクトルとは

簡潔ビットベクトルは、ビットの列を表すデータ構造です。

インデックス 0 1 2 3 4 5 6 7 8 9 ...
ビット列   1 0 1 0 0 1 0 0 1 0 ...

図:ビットベクトルの例

インデックスが0..nの間にある1の数を数える操作をrank(n)と言います。上のビット列で考えると、たとえばrank(2)=2、rank(5)=3、rank(9)=4です。

ただのビット列では、rank(n)を計算するためにO(n)の時間がかかります。しかし簡潔ビットベクトルはとてもかしこいので、rank(n)の問い合わせに定数時間で応えてしまいます。そのために、少し補助データを持つ必要がありますが、そのサイズはビット列自体の20%から25%程度です(ただのビット列に比べると、1.25倍のメモリ消費)。*3

このように、補助データをほとんど使わずに(漸近的にo(n)の補助データで)高速に問い合わせに応えることができるデータ構造を、簡潔データ構造といいます。簡潔ビットベクトルは、簡潔データ構造の最も基本的なものです。簡潔データ構造をうまく使うと、高速全文検索や、圧縮データを展開せずに検索するアルゴリズムなどの応用があります。具体的な実装方法や応用を詳しく知りたい方は、今年発売された『簡潔データ構造』(定兼邦彦 著)などをご参照ください。

簡潔ビットベクトルによる命令情報テーブルの表引き

簡潔ビットベクトルを用いることで、命令情報テーブルの表引きをO(1)にする方法を説明します。命令情報テーブルを再掲します。

PC 行番号
0000 1
0004 2
0008 3

図:命令情報テーブル

PCのインデックスだけ1になった簡潔ビットベクトルを作ります。この例だと、PCは0000、0004、0008なので、"10001000100.."というビット列を簡潔ビットベクトルで表現します。これが索引になります。

インデックス 0 1 2 3 4 5 6 7 8 9 10 ...
ビット列   1 0 0 0 1 0 0 0 1 0 0 ...

図:簡潔ビットベクトルによる命令情報テーブルの索引

あるPCに対応する行番号を得たいときは、rank(PC)を計算します。たとえば0004だったら、rank(4) = 2になります。この値は、命令情報テーブルの上から2番目の行(行番号は2)であることを表しています。PC=0008だったらrank(8) = 3なので、上から3番目の行(行番号は3)になります。簡潔ビットベクトルのrank操作はO(1)なので、命令情報テーブルの表引きが定数時間でできることになります。

簡潔ビットベクトルの分だけメモリ使用が増えることが心配になるかもしれません。しかし、この最適化によって命令情報テーブルからPCの列を消すことができる(二分探索ではPCの情報が必要だった)ので、結果的にはだいたい相殺されます。

実験

N行のメソッドをTracePoint有効下で実行するのにかかる時間を測定した結果が次の図です。

図:線形探索→二分探索→簡潔ビットベクトル

線形探索→二分探索ほどの大きな改善ではありませんが、行数が大きくなったときに二分探索を凌駕していることがわかります。

メモリ使用量に変化がないことも確かめるため、cookpadのWebアプリのソースコードをすべてバイトコードにコンパイルしたときのインタプリタのメモリ使用量を測定しました。実行のたびにブレが発生するので、それぞれ1000回実行したときの頻度を取ってみました。

  • RubyVM::InstructionSequence.compile_fileで測定。
  • /usr/bin/time -f %M kbで測定。

とくにメモリ使用量の傾向が変わっていないことがわかると思います。

まとめ

簡潔ビットベクトルを使って、Rubyの命令情報テーブルの表引き速度を改善しました。これにより、頻繁に命令情報テーブルの表引きを行う条件下での実行(具体的にはTracePoint有効時やコードカバレッジ測定下での実行)が高速化されます。

簡潔データ構造は理論的にも実用的にも非常に興味深い技術で、全文検索やゲノム解析など一部の分野では有名ですが、言語処理系の実装にも活用の可能性があるというのは意外でした*4。みなさんも応用を考えてみるとおもしろいのではないでしょうか。

追記:この話は、クックパッドのフルタイムRubyコミッタである笹田が現状の問題を整理し、遠藤が簡潔ビットベクトルを用いるアイデアを思いつき、笹田が初期の実装を行い、遠藤がそれを改良して実現したものです。フルタイムRubyコミッタが机を並べることでRubyが改良された良い例です。

*1:実は1月にコミットした古いもので、会津大学でのLTイベントで紹介済みだったりもするのですが、記録の意味もこめて記事にしてます。

*2:「ケチな話」と思うかもしれませんが、Rubyではバイトコードのサイズはわりと無視できない関心事のひとつになっています。単なるメモリの無駄遣いというだけでなく、キャッシュを無駄遣いすることになるのでキャッシュミスにつながり、速度低下の原因にもなります。

*3:簡潔ビットベクトルは、n番目の1のインデックスを求めるselect(n)という操作も高速に扱えます。しかし、今回の最適化にselect操作は登場しないので、説明を省略します。

*4:関連研究をご存知の方はぜひ教えてください。

Hackarade #04: Create Your Own Interpreter

技術部の遠藤(@mametter)です。Rubyの開発やってます。

クックパッドでは、Hackaradeという社内ハッカソンを定期的に開催しています。第1回はRubyインタプリタのハック(MRI Internal Challenge)、第2回は機械学習の体験(Machine Learning Challenge)、第3回はISUCON風の社内コンテストを行いました。 4回目となる今回は、遠藤が講師となり、「言語処理系を自作する」というテーマで開催しました。その概要と成果の一部をご紹介します。

f:id:ku-ma-me:20181009143312j:plain

概要

言語処理系の作り方の基本を一日で習得することを目標として、「RubyインタプリタをRubyで書くこと」を具体的な課題としました。

言語処理系は通常、プログラムテキストを抽象構文木に変換する「パーサ」と、抽象構文木を元に指示を実行する「評価器」からなります。今回は、評価器の実装にフォーカスしました(パーサは講師が公開しているminruby gemを利用しました)。

フルセットのRubyを一日で作るのは不可能に近いので、"MinRuby"というサブセット言語をターゲット言語としました。MinRubyは、Rubyの言語機能のうち、次のものだけを持つ言語です。

  • 四則演算、比較演算
  • 文、変数
  • 分岐とループ
  • 関数呼び出し
  • 関数定義
  • 配列、ハッシュ

講師からは、インタプリタの骨格となるスクリプト(interp.rb)とテストケースを提供しました。このスクリプトには多数のraise(NotImplementedError)が含まれています。この穴を上から順に埋めていくと、上記の機能が順に実装されていって、テストケースも徐々に通っていきます。このあたりはゲーム感覚で進められるように配慮しました。

すべてのテストが通ったら、最後の課題は「セルフホスト」です。セルフホストとは、ホスト言語(インタプリタを実装している言語)をターゲット言語にすることです。つまり、interp.rbをRubyではなくMinRubyで書き直します。これにより、「interp.rbをinterp.rbの上で動かす」とか、「interp.rbを動かすinterp.rbをinterp.rbの上で動かす」というようなことが可能になります。

発展課題と成果

セルフホストに成功した後は、自由にインタプリタを拡張してもらいました。正確に数えていませんが、少なくとも10人以上の参加者がセルフホストに成功し、さらにいろいろ拡張してくれました。

f:id:ku-ma-me:20181009142303j:plain

Hackarade当日の夜はパーティで、有志に成果を発表していただきました。その一部を以下に紹介します。

  • 高速化1:case文を直接実装したり頻出ケースを優先したりして3倍以上速くした
  • 高速化2:定数畳み込みや部分評価を実装して実行時の無駄な計算を省いた
  • 型チェッカ:定数参照の構文を型注釈として使い、num :: Integer = "str"などとするとエラーが出るようにした
  • オブジェクト指向:クラスやメソッドを実装した
  • splatの実装:可変長引数の関数呼び出しを実装した
  • ブロックの実装: yieldでdo...endが呼べるようにした
  • 正規表現:マッチングのエンジンから自力で実装した
  • gotoの実装:gotoを実装した
  • MinSwift:Swiftで同じことを再現しようとした(未完)
  • コンパイラ:内部的にバイトコードを生成してからVM実行するようにした
  • 完全セルフホスト:MinRubyでパーサを書き、minruby gemに依存せずにinterp.rb単体でセルフホストするようにした

MinRubyは規模が小さいので、手軽に新機能を実験する土台になります。とはいえ、一日でコンパイラや完全セルフホストに到達する人まで出るとは、正直予想を上回りました。

まとめ

「言語処理系を自作する」というテーマで開催した第4回のHackaradeを紹介しました。もともと言語処理系に詳しいエンジニアから、そもそもRubyをあまり触ったことのないエンジニアまで、幅広い人に楽しんでいただけたと思っています。

Hackaradeはクックパッドのエンジニアの技術力向上を目的としているイベントです。社員エンジニアは原則全員参加(もちろん業務として)、今回はさらにエンジニアアルバイトも業務として参加可能でした。そんなクックパッドに興味を持った方は、募集要項ページをご覧ください。

なお、今回の資料はSlideShareGitHubに公開しています。

https://github.com/mame/cookpad-hackarade-minruby

言語処理系のセルフホストは概念的にはむずかしくないですが、実際にやってみると意外とデバッグがむずかしい(多段になると、どのレベルで例外が起きているのか把握するのに頭を使う)です。読者の方もぜひ一度やってみてください。今回の講義は講師(遠藤)の著書である『RubyでつくるRuby - ゼロから学びなおすプログラミング言語入門』(Amazon)をベースとしています。自力で解くのが難しかった方、答え合わせがしたい方、復習したい方などはご参照ください。

【開催レポ】Cookpad Tech Kitchen #18 生鮮食品EC クックパッドマートの開発秘話

こんにちは。広報部のとくなり餃子大好き( id:tokunarigyozadaisuki )です。

2018年9月26日に、Cookpad Tech Kitchen #18 生鮮食品EC クックパッドマートの開発秘話を開催いたしました。クックパッドでは、Cookpad Tech Kitchenを通して、技術やサービス開発に関する知見を定期的に発信しています。

f:id:tokunarigyozadaisuki:20181010105834j:plain
本イベントの登壇者4名
第18回は2018年9月20日にリリースいたしました、「毎日が楽しみになる、食材店。『クックパッドマート』」の開発秘話をテーマとし、事業開始の背景から初期の価値検証プロセス、アプリのデザイン、iOS開発の進め方についてなどたっぷりとお話をさせていただきたました。 本ブログを通して当日の様子をご来場いただけなかったみなさまにもお届けしたいと思います。

生鮮食品ネットスーパー「クックパッドマート」 

f:id:tokunarigyozadaisuki:20181010105007j:plain

クックパッドマートは、精肉店や鮮魚店、ベーカリーなど地域で有名な店や農家の「こだわり食材」をアプリから購入できる生鮮食品ネットスーパーです。 「焼きたてパン」や「朝採れ野菜」などの新鮮な食材を、販売店から集荷した当日に受け取ることができます。1品からでも送料無料で注文が可能、毎回必要な分だけ手軽に購入することができます。 商品は、提供地域の様々な店舗・施設等に設置された「受け取り場所」の中から、利用者が選んだ場所・時間に受け取ることが可能。そのため、日中忙しくて買い物をする時間がない方でも、新鮮なこだわり食材を手軽に入手することができます。

2018年10月10日現在、商品の受け取り場所は学芸大学駅周辺の「なんでも酒やカクヤス 学芸大学前店」「カラオケの鉄人 学芸大学店」の2店舗となります。都市圏を中心に順次拡大を予定しています。

▶︎アプリダウンロードはこちらから https://itunes.apple.com/jp/app/id1434632076

発表プログラム

「クックパッドマートが目指すもの」

はじめにお話いたしましたのは、クックパッドマートの事業責任者である福崎です。クックパッドマートがどのような課題を解決するサービスであるか、またその課題に向き合うチームの紹介をさせていただきました。 f:id:tokunarigyozadaisuki:20181009193239j:plain

「リリースまでに捨てた10のこと」

デザイナー兼エンジニアで、現在は主にアプリのサーバーサイド実装を担当している長野より、クックパッドマートの事業立ち上げの際に行ったプロトタイピングとその結果をふまえた学びについてお話いたしました。 f:id:tokunarigyozadaisuki:20181010105824j:plain

本登壇内容についてはクックパッド開発者ブログでも記事として公開しております! こちらもぜひご覧ください。 

「クックパッドマートでアプリをデザインした話」

アプリデザインとマーケティングを担当している米田からは、 クックパッドマートをデザインという切り口から振り返り、ラフデザインから実装デザインまでの経緯を発表させていただきました。

f:id:tokunarigyozadaisuki:20181010105827j:plain

発表終盤におまけとしてご紹介しました、クックパッドマート公式キャラクターの「トマート」のTwitterアカウントも、ぜひフォローしてくださいね!

「クックパッドマートのアプリ開発について」

最後は、クックパッドマートのiOSアプリ開発を担う中山の登壇です。 f:id:tokunarigyozadaisuki:20181010105830j:plain なぜアプリを開発することに決めたのか、実際の開発方針や開発速度を上げるために行ったこと、毎日使ってもらえるアプリをつくるために心がけていることなどについて、お話しさせていただきました。

付箋形式でお答えするQ&Aディスカッション

Cookpad Tech Kitchenでは参加者のみなさまからの質問を付箋で集めております。開発体制やスケジュール、iOSアプリのアーキテクチャについてなど具体的な内容から、クックパッドマートの今後の展開などについてなどたくさんのご質問をいただきました。ありがとうございました!

f:id:tokunarigyozadaisuki:20181009193249j:plain
クックパッドマート開発責任者の勝間が司会を担当いたしました

シェフの手作り料理

Cookpad Tech Kitchen ではイベントに参加してくださったみなさまにおもてなしの気持ちを込めて、シェフ手作りのごはんをご用意し、食べながら飲みながらカジュアルに発表を聞いていただけるように工夫しています。

今回は、クックパッドマートで実際にご購入いただける食材でお料理を用意させていただきました!

f:id:tokunarigyozadaisuki:20181010105323j:plainf:id:tokunarigyozadaisuki:20181009195319j:plain
f:id:tokunarigyozadaisuki:20181009193311j:plainf:id:tokunarigyozadaisuki:20181009193307j:plain

おわりに

今回のイベントでは、「毎日が楽しみになる、食材店。『クックパッドマート』」の開発秘話についてご紹介いたしました。 クックパッドマートでは、サービス展開に向けて日々取り組んでいるところですが、実現したいことに対して、まだまだ力が足りていません。そこで、サービス立ち上げにコミットしていただける方を募集しております。

サーバーサイドエンジニア
オペレーションマネジャー
コンテンツディレクター
オープンポジション

https://info.cookpad.com/careers

次回のCookpad Tech Kitchenは、11月1日(木)を予定しております。今後のイベント情報についてはConnpassページについて随時更新予定です。イベント更新情報にご興味がある方は、ぜひメンバー登録をポチっとお願いします!

cookpad.connpass.com