#RubyKaigi 2015 でテンプレートエンジンの高速化について話しました
FamlとHamlitがなぜ高速かについて話しました
RubyKaigi 2015の1日目に、「High Performance Template Engine」というタイトルで発表しました。 @eagletmtさんが作ったFamlと、僕が作ったHamlitがなぜオリジナルのHamlに比べ高速か話しました。
発表は以下のような内容でした。
- テンプレートエンジンとは何か
- テンプレートエンジンの例
- テンプレートエンジンの仕組み
- パフォーマンス
- どのようにRubyのコードを速くするか
- ベンチマークがなぜ必要か
- プロファイリングの方法
- 改善の仕方
- 高速化のために何をしたか
- Famlがやったこと
- Attributeの最適化
- 高速なRuntime Attribute Builder
- Hamlitがやったこと
- String Interpolationのコンパイル
- 使われていない挙動の削除
- Famlがやったこと
以下、発表内で触れられなかったことについて書きたいと思います。
協力しています
同じ会社の人が別々に作って社内で競争しているように見えたかもしれないですがそれは気のせいで、Hamlitは完全に自分の勉強のための趣味プロジェクトとして作っていて、特に社内向けに作っていたわけではないです。
逆にFamlは社内のプロジェクトで使われているため、Famlのユーザーに影響のあるバグを発見した時に僕が直したりとか、Famlの開発初期の段階でKaminariが動くように対応したこともありました。
なので、業務レベルではHamlitを作った経験を生かしてFamlの開発に協力しているという状況であり、特に対立関係にあるわけではないというか、eagletmt先輩は会社では僕の教育担当です。
本家Hamlへの還元に関して
僕はOSSを改善する際は、社内モンキーパッチや野良forkを作るのではなく、元の実装を使わさせていただいたお礼としてupstreamに還元するべきだと思っていて、結構Haml本家を良くしたいという意思は強いです。
実際2ヶ月かけてHamlitをフルスクラッチしたときはパーサーを本家のものに置き変え、次のバージョンのHamlになるつもりで作っていたのですが、HamlにはTempleだと実装しにくい挙動があまりに多く、本家を一度に全てリプレースするのは途中で断念しました。
ただ作っている過程で、現在の挙動を完全に維持しつつ(FamlやHamlitまではいかないものの)ある程度高速化できそうなイメージがついたので、高速化の根幹となるTemple移行のプルリを出したのですが、まだマージされていないというステータスです。
今日Hamlコミッタの松田さんに聞いたところ、現在のメインのコミッタであるNormanしか基本的にマージボタンを押さない運用のため、彼が見るの待ちだそうです。
感想
eagletmtさんと一緒に発表の準備とかしたらテンプレートエンジンに関する面白い話や技術的に深い話が聞けるかなと思って一緒に発表しようと誘ったのですが、準備をしていくうち、本家の実装や仕様に対してどう思っていて何を考えてFamlを作っていたかが聞けてとても面白かったし、大成功でした。
その面白さの一部でも発表を聞いてくれた皆さんに伝わっていればよいなと思います。資料の修正や発表練習に協力いただいた皆さま、RubyKaigiを運営している皆さま、発表を聞いてくださった皆さま、そしてeagletmtさん、ありがとうございました!
#itamae_meetup で「itamaeを楽しく使うための工夫」について発表してきた
Itamae meetup #1 で発表した
趣味でちょくちょくコミットしているItamaeという構成管理ツールの第一回Meetupがあり、Itamaeについてどこかで話したいな〜とずっと思ってたので話してきた。個人的には、僕が作った機能のうち特に最近実装した以下のものを各位に使ってもらうことによって、Itamaeレシピの書かれ方やItamae自体をいい感じにしていきたくて発表した。
itamae generate cookbook
include_recipe
の.rb
省略bundle gem
の命名規則に従ったresource pluginの自動require
話した内容
普段5分だけ話すことが多くてあまり10分話すのには慣れてなかったのだけれど、ノリと勢いで以下のような雰囲気のことを口走っていたら10分経っていた。
去年の ISUCON は itamae でプロビジョニングした。今年は筋肉でやった #itamae_meetup
— Issei Naruta (@mirakui) 2015, 12月 9
「僕には長いコマンド打つの無理なんで」 #itamae_meetup
— c9e (@chiastolite) 2015, 12月 9
レシピからitamaeをぶっ壊せるのが素晴らしい #itamae_meetup
— 奈良阪 (@narazaka) 2015, 12月 9
“テンプレートエンジン書いてると gsub した瞬間に負け” #itamae_meetup
— Issei Naruta (@mirakui) 2015, 12月 9
遅いと思ったらシェルスクリプト書くしか無いww #itamae_meetup
— okumura takahiro (@hfm) 2015, 12月 9
itamaeも高速化してるし発表も超高速だった。 #itamae_meetup
— makoto takagi (@2celeb) 2015, 12月 9
落ちついて話したい
感想
いろんな人にItamaeが愛されてるのが伝わってきて、発表も面白いのが多くて良い会だった。弊社メンバーと発表を聞いてくださった皆さま、ありがとうございました!!
Hamlit v2.0をリリースしました & RubyKaigi登壇します
Slimより高速なHaml実装「Hamlit」
RubyでHTMLを生成するのにERB以外でよく使われるテンプレート言語にHamlやSlimがあります。haml *1 をやめて高速なslimに移行する人が多かったのですが、私はHamlのシンタックスの方が好きなので、slimが用意したベンチマークでslimより高速なHaml実装「Hamlit」を3月にリリースしました。 *2
これはslimが提供しているベンチマークでHTML escapeを有効にし *3、FamlとHamlitを追加したベンチマークの結果です。なおHamlitは完全にHaml互換の仕様ではなく、この非互換が有利に働いています。
互換性と性能が大幅に向上したHamlit v2.0
Hamlitの互換性の問題
Hamlitは最初のv0.1の時点で上記のようなベンチでSlimより高速ではあったのですが、以下のような欠陥がありました。
- HTMLエスケープがない
- エラーがコンパイル時に出る & ランタイムで出ても行番号がズレる
- コーナーケースで属性のレンダリングが重複したり、消えたりする
- 最適化の影響で、書き方が静的か動的かでHTMLのレンダリング結果が変わってしまう
- 空白除去オペレータの挙動がおかしい
社内の20,000個以上のテストケースがあるRailsアプリで使ってみたら5,000個くらいのテストが落ちる状態でした。 以前、これを地道に潰しながら致命的な仕様の見直しを行ったHamlit v1.0.0をリリースしました。v1のどこかのバージョンでそのアプリの全てのテストが通っています。
それでも上記の問題が完全に潰せていたわけではなく、保守性的にも様々な問題を抱えていたため、2ヶ月かけてフルスクラッチを行い、大幅に互換性を改善したHamlit v2.0をリリースしました。
もう一つのHaml実装「Faml」との戦い
Hamlitより先にリリースされたhamlより高速なHaml実装としてFamlがあります。 slimが提供しているベンチマーク(をFamlが公平に参加できるようにしたもの)ではHamlitの非互換性が有利に働きHamlitの方が高速に動作しますが、その他の多くのベンチマークではC拡張を持っているFamlの方が高速に動作する状態でした。
Famlが恣意的に用意したベンチマークでは、1.35倍程度Hamlitの方が遅い状態でした。 さて、 v2.0ではHamlitもC拡張を用意 してC言語レベルでの最適化を行い、上記のような動的なテンプレートでも以下のようにHamlitの方が高速に動作するようになりました。 (Travisでの結果)
hamlit2 v2.0.1: 24225.3 i/s (0.041ms) faml v0.7.0: 19405.8 i/s (0.052ms) - 1.25x slower hamlit1 v1.7.2: 10450.2 i/s (0.096ms) - 2.32x slower haml v5.0.0.beta.2: 5260.5 i/s (0.190ms) - 4.61x slower
でも昨日FamlがC++になって速くなってるので、明日には抜かれているかもしれません。
静的解析エンジン
FamlはRubyの式が静的であればコンパイル時にレンダリング結果を確定させる機構を持っているのですが、Hamlitのそれはかなり貧弱のため、そういったケースでもレンダリング速度で劣っていました。 真面目に実装しなおしたため、上記のテンプレートの値をFamlやHamlitが解析可能な位置にズラしたベンチマークでは、 本家hamlの569倍 の速度が出るようになりました。 (Travis)
hamlit2 v2.0.1: 3639877.1 i/s (0.000ms) faml v0.7.0: 1434853.5 i/s (0.001ms) - 2.54x slower hamlit1 v1.7.2: 17734.8 i/s (0.056ms) - 205.24x slower haml v5.0.0.beta.2: 6388.2 i/s (0.157ms) - 569.78x slower
コンパイル速度の問題
これまで出てきたのは全てレンダリング速度に関するベンチマークですが、本来はコンパイル速度も全体のパフォーマンスに影響し得ます。 Webサービスではあるテンプレートのコンパイル回数に比べてレンダリング回数の方がかなり多くなるためコンパイル速度は無視できますが、Hamlitはコンパイルが非常に遅く、レンダリング回数がそれほど多くないタスクで使うには必ずしもHamlitをオススメできない状態でした。
これはhamlのリポジトリにあるstandard.hamlという少し大きめのテンプレートですが、コンパイル速度でFamlに勝てるようになりました。(Travis) コンパイル時に最適化をあまり行わないhamlにはまだ勝てていません。
haml v5.0.0.beta.2: 297.5 i/s (3.361ms) hamlit2 v2.0.1: 152.6 i/s (6.551ms) - 1.95x slower faml v0.7.0: 139.4 i/s (7.173ms) - 2.13x slower hamlit1 v1.7.2: 78.0 i/s (12.827ms) - 3.82x slower
RubyKaigiでどうやって速くしたか話します
RubyKaigi 2015に、Famlの作者である @eagletmt さんと一緒に登壇します。一つの枠で二人のアイデアが聞けるのでお得です。 どう速くしたのかの詳細はあえてこの記事では書きませんでしたが、何を考えて作ったのかとかも含めRubyKaigiで話す予定です。是非聞きに来てください!!
*1:言語に関してはHaml、gemに関してはhamlで表記します
*2:Hamlit v0.1.0リリース時の記事: http://k0kubun.hatenablog.com/entry/2015/03/31/004021
*3:元のベンチではSlimもERBもRubyのスクリプトに対してHTML escapeを行わないオペレータを使っているんだけど、HamlにはHTML escapeされるオペレータを使っているのでSlimやERBでもHTML escapeされるように直してる。HTML escapeしない側に倒さないのは、ベンチに加えたいFamlにはHTML escapeしないオプションがないのと、Railsの標準だとescapeされるため。
byebugやpry-byebugを使った後の挙動を10倍高速にしました
byebugとpry-byebugのbundle updateをしましょう
byebugはRails標準でインストールされるRuby 2.1, 2.2向けのデバッガで、pry-byebugはpry *1 にデバッガの機能を追加するpryのプラグインです。 一昨日から今日にかけて、以下のパフォーマンス改善を含む byebug v8.0.0 と pry-byebug v3.3.0 がリリースされました。
byebugとpry-byebugには、一度byebug
やbinding.pry
を叩くとそれ以降ずっとアプリケーションの挙動が10倍遅くなるという問題がありました。
これが上記の変更により改善されたので、 Railsアプリやgemのデバッグにbyebugやpry-byebugを利用している方はそれらのbundle updateをおすすめします。
binding.pryすると10倍遅くなる問題
Byebugは行の移動やブレークポイントで処理を止めたりするため、Ruby 2.0から導入されたTracePoint *2 *3 というRubyのVMで起きるイベントに処理をフックさせるためのAPIを使っています。
byebugとpry-byebugはbyebug
やbinding.pry
を叩いたときにByebug.start
というメソッドの中で必要なTracePointを設定することによってデバッグを可能にしているのですが、 いままでのbyebugやpry-byebugはデバッグ終了後にデバッグ用のイベントフックを無効にしていなかった ため、一度byebug
やbinding.pry
をするとそれ以降どんなアプリでも10倍程度遅くなってしまう状態でした。
なぜそんな状態だったのか
byebugとpry-byebugの作者はこの問題とどうしたら直るかを認識していたのですが、以下の理由により簡単には解決できない状態でした。
- TracePointを無効にするための
Byebug.stop
がバグっていて、使うと大量にテストがfailする - TracePointを複数設定した後それらを無効にすると、freeしたTracePointを参照してしまうRuby自体のバグがありランダムにSEGVする
これらの問題をgdbを使って地道にデバッグを行って原因を特定し冒頭に載せたパッチによって問題が解決したため、デバッグ終了時にByebug.stop
を呼ぶことが可能になりパフォーマンスが大幅に改善されました。なお、Ruby自体のSEGVに関しても原因を特定しパッチを投げています。
注意点
byebugやpry-byebugにはブレークポイントを設定する機能があるのですが、それを設定している間はByebug.stop
しない(したらブレークポイントが動かなくなる)ためブレークポイントを使っていると遅いままになります。解除すればまた速くなります。
なお、Byebug.stop
されているかはアプリケーション上でByebug.started?
がfalse
を返すかを見ることで確認することができます。 false
が返れば高速に動作しています。
所感
直すの結構大変だったので是非最新のbyebugとpry-byebugを使っていただきたい気持ちです。
ISUCON5予選に「railsへの執着はもはや煩悩の域であり、開発者一同は瞑想したほうがいいと思います。 」チームで参加してきた #isucon
学生枠で出場した去年に引き続き、今年は新卒社員になった@cnosukeと@rkmathiと僕@k0kubunの3人でISUCON5予選に出場した。
僕らが最後に確認できたスコアは11045で、その時のスクショだと2日目の16位という感じで、予選通過はなりませんでした。
id:rkmathiの記事: http://rkmathi.hatenablog.com/entry/2015/09/28/004734
戦略
最初にコードは読まず、nginxのアクセスログをパースして集計する奴とnewrelicを使ってベンチ中最も時間を使っているエンドポイントを特定し、そのエンドポイントの中でどこが遅いかはrack-lineprofで把握してそこだけ読んで潰していく感じでやった。
去年の感じだと予選はいきなりキャッシュとか入れなくても落ちついて普通のチューニングをしていれば突破できると思っていたので、地道にSQLのチューニングをしていた。
僕らのチームがやったこと
時系列で並べるのが好きなのでそれで書く。
cnosukeがインフラレイヤーをやって、rkmathiがMySQLのログ集計とかインデックス直しをやって、僕がクエリ変形させたりひたすらN+1を潰すのをやっていた。
他は思いついた奴を出しあって手があいてる人がやる感じだった。
score | やったこと |
---|---|
200 | Rubyの初期状態(大体) |
800 | nginxで静的ファイル配信 |
926 | relationsのINSERTをone < another保証にしてクエリの条件減らした。created_atのソートを全てidにした。 |
1192 | footprintsにインデックスを貼った |
1264 | InnoDBのバッファのサイズを上げた |
2947 | relationsのoneとanotherにインデックス貼った。セッションのクッキーストアの暗号化をやめた。 |
6202 | "/" へのリクエスト用にis_friend?のN+1用preloader (JOINではなくクエリ2つで引く)を書いた。entriesのレスポンスをRedisにキャッシュするようにした。 |
9557 | LIMIT 1000を10個に絞るクエリ2つを両方LIMIT 10で引き、commentsのSELECTのrowsが減るインデックスを貼り、Redisのチューニングをした |
9610 | firstしてる系にLIMIT 1を付加 |
10194 | friend idsを引くN+1をなんとかした |
7200 | 再起動しただけで点数が減った |
7963 | N+1になるget_userの汎用的なpreloaderを書いて適用 |
8402 | ベンチを回す度に点数が回復していた |
10000 | friend idsを引くクエリをRedisのsetに変えた(けどこの辺でボトルネックがIOになっていて、期待よりスコアが伸びていない) |
11045 | 遅いエンドポイントのビューの中のget_userとかのN+1を潰した。 |
最初に10000点まで行くのは結構早くて順調だったんだけど、RedisとMySQLのウォームアップがちゃんと書けず、CPUもメモリも使いきらない状態のまま提出となった。
この辺の経験不足が敗因なのかなあと思っているのと、正直このラインまでやれば予選抜けられると思っていたので、レベルが上がってるような気もした。
感想
めちゃくちゃ楽しかったし、ずっと集中しててすごい疲れた。去年は学生枠でギリギリ本戦に出してもらった感じだったけど、今年は去年よりちゃんとチームで連携してスコアが上げられて、メンバーの成長が感じられてよかった。
今年結構テンプレートエンジン周りを触ってて、その辺練習の時は最適化とかやってて今回の問題でも直せるところあるなと思ったけど、本番ではそのレイヤーにボトルネックが移せなかったのが悔しかった。
次から次へと直すところが出てくる面白い問題で、出題や運営をしてくださった方々には感謝ばかりです。是非来年も参加したいです。
#tqrk09 で「Rubyを使った開発環境構築の自動化」についてLTしてきた
TokyuRuby会議09でLTした
あんまり外部でRubyについて話したことなくて、こないだshibuya.rbでactiverecord-precountについてLTしたけど、それ以外にもまだ紹介したいgemがあったのでTokyuRuby会議にLT応募したら通ったので話してきた。
rebuild gem
OSXでgitのインストールとdotfilesのgit cloneとその中のスクリプトの実行までやってくれるgem。一度セットアップしたあとは、新しいのをgit pullしてきてgitconfigで指定しておいたスクリプトを走らせるのに使う。僕のgemの中だと結構ダウンロードされている奴で、今でも普段普通に使っている。
実行するスクリプトにRubyのライブラリ使うとメンテがダルいみたいな話をしたんだけど、適材適所だと思うので、どちらかだけ使っていれば良いみたいな話でもないとは思っている。あるいは自分が使ってて楽しいgemを使うのが良いと思う。
AppleScriptでGUI操作してるんだけど、boxenとか見ると別のやり方もすでにあったので、Macで自動化をするときは先にboxenとかを見ると良い。
karabiner-dsl gem
これはMacのキーの設定をするKarabinerというユーティリティのXMLの設定ファイルを吐き出すRuby DSL。日常的に頻繁にキーバインドをいじる人間なので、これもよく使っている。これを使って普段どんな最悪なキーバインドを設定しているかを紹介した。やめたい。
エディタ何使ってるんだろうみたいなツイートを見かけたけど、vimを使っていて、vimのキーバインドを潰してしまっている部分はvimrcで別のものに設定している。とにかく最悪という感じがする。
RubyのDSLでキーバインドの変更履歴を管理したいみたいな人には向いてると思う。
TokyuRuby会議に初参加した感想
スポンサーついててビールが無限に出てくるのやばい。あと持ち寄りの料理もすごいおいしかった。おいしいもの飲み食いしながらいろんな人と話せて最高の会だった。来年も参加したい。
ElectronでYoruFukurou風のTwitterクライアントを作った
最強のTwitterクライアント戦争
なんか戦争をやってる人たちがいたので乱入することにした。
日記書いた / 最強のTwitterクライアント作り始めた - 9mのブログ http://t.co/ecq7nT40qp
— 友緒利奈 (@9m) August 22, 2015
最強のTwitterクライアント戦争参戦したい
— 戦車 (@r7kamura) August 22, 2015
戦争だ…! / “最強のTwitterクライアント戦争に参戦 - ✘╹◡╹✘” http://t.co/iJ3BAgPvFI
— 友緒利奈 (@9m) August 22, 2015
YoruFukurou風のTwitterクライアント
最近デスクトップでLinuxを使い始めたんだけど、YoruFukurouみたいな感じで使えるTwitterクライアントがなくて困っていた。
なので、YoruFukurouを再実装した。Mac環境で本家を置き換えようとは思ってなくて、あくまでWindowsとかLinuxとかでYoruFukurouを使いたい人向けに作っている。
デモ
インストール方法
https://github.com/k0kubun/Nocturn/releases
からzipでダウンロードできるようにしてある。
これからカスタマイズするつもりはあるけど、現状はパクりアプリなのでApp Storeに出す予定はない。
実装されている機能について
下記のショートカットで使える機能と、画面に見えてるボタンの機能が実装されている。
Key | Command |
---|---|
Enter | ツイートの投稿、リプライ先の選択、検索実行 |
j, ↓ | 次のツイートを選択 |
k, ↑ | 前のツイートを選択 |
0, space | 最初のツイートを選択 |
f | お気に入りに追加 |
Cmd-backspace | ツイートを削除 |
自分がよく使う機能しかまだ実装してない。面倒そうなのもちょっと省いているけど、8割くらいのユースケースは満たせる気がする。
なぜNocturnなのか
中二病です
Electron使ってみた感想
僕はJavaScriptの文法を知らない(CoffeeScriptを書く)くらいフロントエンド力が低いけど、Electron使ったことない状態から始めて1〜2日で完成したのでかなりわかりやすくて使いやすいと思う。
HTMLとCSSとJavaScriptをちょっと書くだけでデスクトップアプリがサクサク作れるのでめちゃくちゃ楽しい。みんな絶対やったほうがいい。
あと、Macで開発してたけどWindowsとかLinuxで起動したら普通にうごいた。すごい。
というわけで、Electron触ってみたい人はぜひNocturnをいじってみてください。