こんにちは!バックエンドチームマネージャーの@tsuwatchです!
2022/9/8〜10に三重県にて開催されたRubyKaigi 2022でプラチナスポンサーとして協賛し、スポンサーブースを出展しました。
弊社からはWEARを開発するバックエンドエンジニア、SRE、PdMなど合計10名ほどが現地で参加しました。
我々が運営しているファッションコーディネートアプリ「WEAR」のバックエンドはRuby on Railsで開発しています。2013年にVBScriptで作られたシステムですが、2020年くらいからVBScriptのシステムをコードフリーズし、リプレイスをはじめました。現在もリプレイスを進めながら、新規の機能もRubyでどんどん開発しています。
また弊社ではRubyコミッタのsonotsさんがいたり、顧問としてMatzさんにもご協力いただいており月に一度、Matzさんに何でも聞く会をやっていたり、積極的にRubyを活用しています。
今回もエンジニアによるセッションの紹介とブースでの取り組みについて紹介します。
エンジニアによるセッション紹介
弊社エンジニアによるセッションの紹介をします。
How fast really is Ruby 3.x?
高久です。Fujimoto Seijiさんのセッション「How fast really is Ruby 3.x?」についてご紹介します。
このセッションでは、Rubyの過去バージョンと比較しRuby 3.xではどれくらい早くなったかを、実際のアプリケーションを用いて検証した結果を報告しています。検証対象のアプリケーションはFujimoto Seijiさんがコミッターを務めるFluentdです。
検証の背景について。まず前提としてRuby 3は「Ruby 3×3」という「Ruby 2.0の3倍早くする」ことを目指して開発が進められました。そして過去のRubyKaigiでも同様に実際のアプリケーションを使った検証の話がいくつかありました。しかし、どれもRailsアプリで作られたものでは3倍にならないという話でした。なぜならRailsアプリは基本的に、アプリケーションの多くの時間を占めているのがRubyの処理ではなく、DBなどの外部処理によるものだからです。Rubyを早くしたとしても、その他にかかる時間が大きいため、全体の処理時間の短縮化に大きく寄与するものではありませんでした。
そこで今回、多くの処理をRubyで行なっているFluentdを対象に測定してみたらどうなるか? というのがテーマとなっています。
検証は読み込ませるファイルごとに2パターン行なっています。
- LTSVファイル
- nginxログファイル
検証した結果Ruby 1.9と比較しRuby 3.2(YJIT有効)は、LTSVファイルで約3.15倍、nginxログファイルで約2.5倍のスループットが出ることを確認できました。Ruby 1.9と3.2の比較にはなりますが、概ね「Ruby 3×3」は実現しているのではということが、Fujimoto Seijiさんが伝えたかった内容になります。
セッションでは、更に他の言語と比較してどうかを述べていました。気になる方は是非スライドをご確認ください。
コミッターさんたちがRubyをより良くするために開発をし、Rubyが日々進化していることを実感した発表でした!
Make RuboCop super fast
小山です。RuboCopメンテナの@koicさんによる「Make RuboCop Super fast」の発表を紹介します。
RuboCopは2012年4月21日がファーストコミットで今年10周年を迎えました。RuboCopは現在1.36.0が最新バージョンでありますが、2.0系リリースに向けてマイルストーンを掲げており、この発表はそのマイルストーンのうちの1つであるRuboCop2×2に関する発表でした。RuboCop2×2はRuby 3×3で目指しているように、RuboCopの速度を1.0系と比較し、2倍を目指すというものです。
RuboCopはCaching、Multi-cores、Reduce unused requires、Daemonizeの4つのアプローチで高速化を図っています。
Cachingはかねてから提供されていて、1回検査したコードはデフォルトで ~/.cache/rubocop_cacheに保存しています。
Multi-coresは1.19からデフォルトで並列検査するようになり、1.32から並列でオートコレクションするようになりました。8 core CPUかつHyper-Threadingを使用して約1,300ファイルに対して直列実行と並列実行を比較した場合、直列実行は61秒で完了したのに対し、並列実行は10秒で完了したそうです。
Reduce unused requiresは --onlyオプションを付与したとしてもすべてのCopが読み込まれてしまう問題がありました。require 'rubocop' の改善により高速化を実現しており、セッションの本論で話されていたserverモードと一部関連があります。
Daemonize(serverモード)がセッションで一番厚くお話されていた内容になります。serverモードは#10706で対応されて1.31から導入されています。使用することでrubocopコマンドを実行する度にプロセスを起動するのではなく、プロセスを常駐することでモジュールの読み込みが初回のみになります。Client/Serverモデルを採用して高速化を実現していたサードパーティ製のgem、rubocop-deamonを統合することで、RuboCopのserverモードは高速化されています。Client/Serverモデルとは、Server側の初回プロセスであらかじめモジュールを読み込んでおいて、Client側がすでにモジュールを読み込んでいるサーバーに接続するアプローチです。またrubocop-daemonをどのように統合したか、serverモードの設計、具体的な使用方法についてはセッション内で詳しく解説されていますので気になる方は是非スライドをご覧ください。
成果としては、moduleの読み込みを必要なもののみにし、serverモードを実装したことで850倍高速化されています。驚くべき成果です。
RubyのDX向上に、すぐに繋がると実感した素敵な発表でした。まだ不安定な挙動が残っているとの補足はありましたが、RuboCopのバージョンを上げて積極的に使っていきたいです!
The Better RuboCop World to enjoy Ruby
三浦です。私からはOhba Yasukoさん(@nay3)によるセッション "The Better RuboCop World to enjoy Ruby" について紹介したいと思います。技術的なセッションからは少し視点を変えた、RuboCopとうまく付き合っていくにはどうしたら良いかを考える内容になります。
RuboCopはRubyの静的コード解析ツールの1つで、コーディング規約を守れていないコードを簡単に確認でき、自動で修正できる便利なツールです。CIで回して事前に修正しておくことでレビューの負担軽減にも繋がります。
ただこのRuboCopのルールは "状況に合わないこと" もあります。例えば "Naming/PredicateName" というルールは、has, is, have_ といった特定の接頭辞のメソッド名をチェックし、接頭辞を排除したメソッドを使うよう警告します。
# bad def is_child? end def has_child? end # good def child? end
ただ child?
にしてしまうと、どちらとも捉えられるような曖昧なメソッド名になってしまいます。
- "is child" なことをチェックするメソッド
- "has child" なことをチェックするメソッド
このようにRuboCopの中にはルールとして間違ってはいないけど状況によっては合わないルールがいくつか存在しています。
初心者〜初級者のエンジニアの場合、状況に合っていないルールなのかを判断するのは難しいです。そのためRuboCopの警告に忠実に従ってコーディングをしてしまい、その結果かえって読みにくいコードになってしまう場合があります。
レビューで指摘された軽微な修正でも直そうとするとRuboCopのルールに引っかかってしまい、複雑な実装になってしまったなんてこともよくあります。(私もありました、、、) このような状況に合わないルールで振り回されてしまうのは、開発速度を低下させる一因にもなります。
RuboCopの全てのルールは、無効にしたりデフォルトとは異なる方針に変更できます。また、特定のコードで特定のルールを無視できます。しかしこのルールの設定の判断はルールの妥当性を判断できる技術力と経験が必要です。初心者〜初級者のエンジニアにとってはこの判断はなかなか難しいものです。かといって経験者が1つ1つのルールを必要か毎回確認するのも大変です。
そこで提案されたのがルールを大きく2つのレベルに分けて考えることでした。
- 強制レベル:ほぼ100%の状況で適用しても問題がないようなルール
- 参考レベル:なるべく多くの改善ポイントに気付けるような理想的なルール
この2つのレベルに合わせてrubocop.ymlの設定ファイル自体を分けておきます。強制レベルのルールは現状通りCIなどで警告を出し、修正を強制します。参考レベルのルールはCIを回しますが、参考情報として表示するだけに留めておきcommitの禁止やマージの禁止といった強制力は持たせないようにします。
このように参考レベルのルールを作っておくと全てのルールに従おうとして不自然なコードを作ることを防ぐことができるので良いのではということでした。Ruby初心者にとってのつまづきポイントなどもたくさん紹介しており、うなずきたくなるような共感できる内容でした。スライドのイメージ図は全て画像生成AIのMidjourneyを使って生成したものだそうで笑いも起こる楽しいセッションでした。
Implementing Object Shapes in CRuby
@tsuwatchです。個人的におもしろそうだなと思っていたObject Shapesというオブジェクトのプロパティを表現する手法について書こうと思います。Object Shapesを導入することでインスタンス変数のを見つけるときのキャッシュヒット率の増加とランタイム時のチェックを減らすことができ、JITのパフォーマンスを向上させるというものです。また、この手法はTruffleRubyやV8で採用されているそうです。
詳細はチケットにあるのでご覧ください。
Object Shapesとは
class Foo def initialize # Currently this instance is the root shape (ID 0) @a = 1 # Transitions to a new shape via edge @a (ID 1) @b = 2 # Transitions to a new shape via edge @b (ID 2) end end class Bar def initialize # Currently this instance is the root shape (ID 0) @a = 1 # Transitions to shape defined earlier via edge @a (ID 1) @c = 1 # Transitions to a new shape via edge @c (ID 3) end end foo = Foo.new # blue in the diagram bar = Bar.new # red in the diagram
例えばこういうコードが存在したときにObject Shapesは以下のようなツリー構造を構築します。 インスタンス変数の遷移をツリー状に構築することで、同じ遷移をするクラスはキャッシュを利用できます。別のクラスをnewしたときにも元のShapesの差分のみ作れば良いというわけです。
class Hoge def initialize # Currently this instance is the root shape (ID 0) @a = 1 # Transitions to the next shape via edge named @a @b = 2 # Transitions to next shape via edge named @b end end class Fuga < Hoge; end hoge = Hoge.new fuga = Fuga.new
現在はクラスをキャッシュキーとして使用しており、その場合はこのコードではキャッシュヒットさせることはできません。Object Shapesを導入することでクラス依存しないキャッシュを実現し、キャッシュヒット率を上げることができます。
複雑そうですが、効果がありそうなパフォーマンスチューニングです。キャッシュの構造や実際にどれくらいパフォーマンスが改善するのか今後もウォッチしていこうと思います。
Method-based JIT compilation by transpiling to Julia
近です。@KentaMurata氏のメソッドベースのJust-In-Timeコンパイルへの新しいアプローチとして、インフラストラクチャにJulia言語を使用した背景と仕組み、特徴についてのお話を紹介します。Numo::NArrayやRed Arrowを使えば大きな数値計算が出来つつありますが、MJITやYJITが利用できてもあまり高速ではないという問題があるとのことでした。
理由として、これらのJITコンパイラがRubyの全てのセマンティクスを保持するためです。Rubyでは全てのメソッドが再定義可能で、再定義されたメソッドは直ちにコード実行に影響を与えます。例えば、以下のようなループの途中でも、injectメソッドや+演算子(メソッド)が再定義されていないかの確認が毎回行われます。
s = (1..10).inject { |a, x| a + x }
以上の特徴によって高速性が失われていましたが、数値計算の場合このRubyの動的性は数値計算アルゴリズムでは殆ど無意味なので、これを無視して計算を最適化できるほうが良いのでは? というのがこのセッションの議題になります。
しかし、現在ではこのような最適化を行うためには、アルゴリズムをCの拡張ライブラリに書き換える必要があります。これをせずに、高速化を行う手段として挙げられたのがJuliaでした。Juliaはデータ処理や数値計算に向いていて高速な言語という特徴があります。
ここで、比較としてPythonの世界での解決策であるNumbaというライブラリの紹介がありました。NumbaはCPython用のJITコンパイラであり、PythonとNumPyのコードの一部を高速な機械語に変換します。もう少し具体的にコンパイルの流れを書きます。
- CPythonのバイトコードを解析
- NumbaIRを生成して書き換え
- 型を推論
- 型付IRに書き換え
- 自動並列化の実行
- LLVM IRの生成
- ネイティブコードにコンパイル
という流れで、CPythonを型付の中間表現に変換してネイティブコードを生成しています。
またNumbaには2つのモードがあります。オブジェクトモードというCPythonインタプリタのC APIを使用しCPythonの完全なセマンティクスを保持するモード。もう1つはnopythonモードというfloat64やnumpy配列などの特定のネイティブデータ型に特化した小さくて効率の良いネイティブコードを生成するモードです。ざっくり言うと処理をPython経由で行うか、CPUに直接命令するかの違いになります。
このNumbaのnopythonモードのようなものをRubyのJITコンパイラでも実現する手段として登場するのが、今回のセッションの本題であるJuliaでした。まず、RubyでNumbaライクなJITコンパイラを表現すると以下のようになります。
- Rubyメソッド
- ASTの生成
- 最適化
- バイトコードの生成
- CRubyのバイトコード
- IRの生成
- タイプ推論
- 最適化
- 型付けされたIR
- LLVM IRの生成
- ネイティブコードにコンパイル
これと同じようなことをやっているのが、Juliaになります(JuliaはNumbaともだいたい同じ機構が動いています)
- Juliaコード
- ASTの生成
- Julia AST
- タイプ推論
- IRの生成
- Julia typed IR
- LLVM IRの生成
- ネイティブコードにコンパイル
そこで、RubyをJuliaへトランスパイルし、それ以降をJuliaのJITコンパイラに実行してもらって高速化します。
- Rubyメソッド
- Juliaコード
- Julia AST
- Julia typed IR
- LLVM IRの生成
- ネイティブコードにコンパイル
Juliaは最適化されたネイティブコードを生成してくれるため、高速です。この辺りの解説はセッションのスライド図と発表が大変分かりやすく、面白かったので是非資料や動画をご覧ください。
次に、RubyからJuliaへのトランスパイル方法についての紹介がありました。トランスパイルには、yadriggyというRubyメソッドのASTを構築し、構文と型をチェックするgemを使用しているとのことでした。セッションでは具体的なコードを紹介していましたが、大雑把にいうとRubyコードからASTを構築し、それを使ってJuliaコードを生成しているようでした。
Rubyコード → Ruby AST → 型チェッカーでノードに対して型付け → Juliaコード
これによって、Rubyの機械への命令を最適化させています。
またRubyとJuliaで実装の違うものがいくつかあり(例ではRangeを挙げていました)これらの対応をするには自分で変換コードを書く必要があるとのことでした。
これらの高速化の実験比較として、以下の計算をしていました。
- マンデルブロ集合
- モンテカルロ法によるπの近似
- クイックソート
- 畳み込み
- ドット積
それぞれの結果は以下のようになっていました。RubyはおそらくYJITが有効。
- マンデルブロ集合
- Ruby:平均3.326ms
- Ruby to julia:平均171.667μs
- モンテカルロ法によるπの近似
- Ruby:平均106.368ms
- Ruby to julia:平均8.851ms
- クイックソート
- Ruby:平均7.551ms
- Ruby to julia:平均1.937ms
- 畳み込み(結果が複雑なので省略)
- ドット積
- Ruby (N=10000)
- 平均468.608μs
- Ruby to julia (N=10000)
- 平均3.759ms
- Ruby to julia (N=10000, T=Float64)
- 平均10.651μs
- Ruby (N=10000)
これらのように、一部の結果を除いて超高速に計算することが可能になるようでした。
紹介は以上になります。発表が分かりやすく、深掘りしたくなるような興味深い内容でした!Rubyの高速化についてこういう方法もあるんだなと、とても学びが多かったです。RubyKaigiではこのような発表がいくつもあり、すごくワクワクしました。
スポンサー
今年も2019年に続いてスポンサーブースを出展しました。
こちらは今回のために作成したノベルティたち。すごくかわいいですね!来てくださった方にもとてもご好評いただきました。とてもこだわって作成したので嬉しかったです。
Tシャツもデザイナーさんにデザインしていただいた魂のこもったTシャツです!普段でも全然着られるのではないでしょうか?「WEAR」アプリをインストールしていただいてる方にお配りしていたのですが、1日目でほとんどなくなってしまいました。ありがとうございます!着てください!
またブースでは、会期中の3日間『エンジニアのファッション事情を大調査!』と銘打ち、毎日異なるアンケートを取っていました。
服を買うなら?
- 実店舗
- 28票
- ECサイト(ZOZOTOWN)
- 15票
- ECサイト(ZOZOTOWN以外)
- 9票
- その他
- 3票
まだまだ実店舗が多いですね!次はありがたいことにZOZOTOWNでした!ありがとうございます!その他の方はご家族やパートナーの方が買っているとの声もありました。
コーディネートはどうやって決める?
- 己のセンスを信じる
- 86票
- 雑誌やネット
- 56票
- その他
- 45票
- 周りの人を見る
- 19票
己のセンスを信じる人が多かったです。エンジニアはやはり我が道を行くのでしょうか。
コーディネートのパターン数は?
- 1 ~ 5
- 43票
- 着るときに考える
- 41票
- 6 ~ 10
- 37票
- 10より多い
- 9票
票がわかれました。ちなみに僕は着るときに考える派です。でもそんなに服がないのでいつもだいたい同じ格好になってますね。
みなさんアンケートに回答していただきありがとうございました!
我々はファッションの悩みを解決することをミッションとして掲げています。みなさんが日々どのようにファッションと向き合っているのか、いろいろお話を聞くことができました。「WEAR」に要望をくださったり、使ったことがない人にご紹介できたりしました。ファッションへのモチベーションが高い人も、そこまで高くない人もそれぞれ悩みはあると思います。「WEAR」をこれからも良いサービスにしていきます!
最後に
ZOZOではセミナー・カンファレンスへの参加を支援する福利厚生があり、カンファレンス参加に関わる渡航費・宿泊費などは全て会社に負担してもらっています。ZOZOでは引き続きRubyエンジニアを募集しています。以下のリンクからぜひご応募ください。
また、メドピアさん、ファインディさんと合同で9/27に「After RubyKaigi 2022」を開催します。ぜひご参加いただければと思います。
おまけ
楽しんでいる様子です。Wポーズ!
PdMのお二人! 昔の仕事仲間や他社の方々、ユーザーさんと交流できて楽しそうでした。
Matzさんと! 全員集合したかったですね。
RubyKaigiではおいしいご飯が食べられます!
アンドパッドさんの二進数足し算RTAで弊社のjeuxd1eauが5位!なんとPdMの方です!エンジニア、本部長も敗北しました。
待ちに待ったオフラインでのカンファレンスで久しぶりに良い刺激をもらいましたし、とても楽しかったです。来年は松本市でお会いしましょう!それではまた次回。