k0kubun's blog

railsへの執着はもはや煩悩の域であり、開発者一同は瞑想したほうがいいと思います。

CRuby向けのLLVMベースのJITコンパイラを書いている話

LLRBというRuby向けのメソッドJITコンパイラを書いている

github.com

RubyKaigi 2015の最後のキーノート@evanphxが「LLVMでCRubyのコードをインライン化するメソッドJITを実装したら速いんじゃね」みたいな発表をしていたのを覚えているだろうか。

LLRBというのはまさにそれを実装しているプロジェクトであり、少なくとも現時点で「LLVMでCRubyのコードをインライン化するメソッドJIT」と言える状態まで実装でき、ものによっては効果が出る状態になったので公開した。

なんで書いてるの

言語を自分で実装するとその言語に関する理解が大分深まる、というのをHamlの実装とかCコンパイラとかで体験していて、僕が一番好きな言語はRubyなのでRubyでもそれをやっておきたい、というのがあった。また、Rubyは遅いと言われがちだが、どこに改善可能な点が眠っているのかにも興味がある。

また、ささださんがRuby3のゴールの一つにJust-in-Time compilationをあげていて、そのスライドで1つの選択肢として言われていた"Use LLVM"というパートを検証するものでもある。ただし、MatzがLLVMはDependency的に微妙と言っており、本体にこの方向性で入ることは多分なく、あくまで実験的なプロジェクトになる。

どのように動くか

READMEを書いてたら大分疲れたので、この記事はその日本語訳にしておく。

LLRBのビルドプロセスでは、CRubyの命令やそれに使われるいくつかの関数をLLVM bitcodeという形式にプリコンパイルしている。

 ________     _________     ______________
|        |   |         |   |              |
| CRuby  |   | CRuby   |   | CRuby        |
| C code |-->| LLVM IR |-->| LLVM bitcode |
|________|   |_________|   |______________|

LLRBのJITのためのプロファイラが開始されると、Rubyの実行中にJITコンパイルがスケジューリングされる。 JITコンパイルの際、LLRBはYARV ISeqをLLVM IRにコンパイルするが、その際にプリコンパイルしておいたLLVM bitcodeをリンクするので、LLVM Passによる関数のインライン化やその後の最適化が行なえる。

最適化を行なった後、LLVM IRはLLVMのMCJITエンジンによって機械語に変換され、それを呼び出すためのCの関数ポインタとしてLLRBに返される。コンパイル対象のISeqに対し、後に説明する方法によって変化が加えられ、この関数ポインタが実行されるようになり最適化が完了する。

 ______     ______________      __________      ___________      _________
|      |   |              |    |          |    |           |    |         |
| YARV |   | ISeq Control |    | LLVM IR  |    | Optimized |    | Machine |
| ISeq |-->| Flow Graph   |-*->| for ISeq |-*->| LLVM IR   |-*->| code    |
|______|   |______________| |  |__________| |  |___________| |  |_________|
                            |               |                |
                            | Link          | Optimize       | JIT compile
                      ______|_______     ___|____          __|____
                     |              |   |        |        |       |
                     | CRuby        |   | LLVM   |        | LLVM  |
                     | LLVM Bitcode |   | Passes |        | MCJIT |
                     |______________|   |________|        |_______|

実際にパフォーマンスが向上するか?

基本的なインライン化は実装済なので、その効果を見ていく。

以下のようなruby/benchmark/bm_loop_whileloop.rbのコードについて考える。

def while_loop
  i = 0
  while i<30_000_000
    i += 1
  end
end

このメソッドのYARV ISeq (LLRBのコンパイル対象) はこうなっている。

> puts RubyVM::InstructionSequence.of(method(:while_loop)).disasm
== disasm: #<ISeq:while_loop@(pry)>=====================================
== catch table
| catch type: break  st: 0015 ed: 0035 sp: 0000 cont: 0035
| catch type: next   st: 0015 ed: 0035 sp: 0000 cont: 0012
| catch type: redo   st: 0015 ed: 0035 sp: 0000 cont: 0015
|------------------------------------------------------------------------
local table (size: 1, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 1] i
0000 trace            8                                               (   1)
0002 trace            1                                               (   2)
0004 putobject_OP_INT2FIX_O_0_C_
0005 setlocal_OP__WC__0 3
0007 trace            1                                               (   3)
0009 jump             25
0011 putnil
0012 pop
0013 jump             25
0015 trace            1                                               (   4)
0017 getlocal_OP__WC__0 3
0019 putobject_OP_INT2FIX_O_1_C_
0020 opt_plus         <callinfo!mid:+, argc:1, ARGS_SIMPLE>, <callcache>
0023 setlocal_OP__WC__0 3
0025 getlocal_OP__WC__0 3                                             (   3)
0027 putobject        30000000
0029 opt_lt           <callinfo!mid:<, argc:1, ARGS_SIMPLE>, <callcache>
0032 branchif         15
0034 putnil
0035 trace            16                                              (   6)
0037 leave                                                            (   4)
=> nil

LLRBのコンパイラは、これを以下のようなLLVM IRにコンパイルする。

define i64 @llrb_exec(i64, i64) {
label_0:
  call void @llrb_insn_trace(i64 %0, i64 %1, i32 8, i64 52)
  call void @llrb_insn_trace(i64 %0, i64 %1, i32 1, i64 52)
  call void @llrb_insn_setlocal_level0(i64 %1, i64 3, i64 1)
  call void @llrb_insn_trace(i64 %0, i64 %1, i32 1, i64 52)
  br label %label_25

label_15:                                         ; preds = %label_25
  call void @llrb_insn_trace(i64 %0, i64 %1, i32 1, i64 52)
  %2 = call i64 @llrb_insn_getlocal_level0(i64 %1, i64 3)
  call void @llrb_set_pc(i64 %1, i64 94225474387824)
  %opt_plus = call i64 @llrb_insn_opt_plus(i64 %2, i64 3)
  call void @llrb_insn_setlocal_level0(i64 %1, i64 3, i64 %opt_plus)
  br label %label_25

label_25:                                         ; preds = %label_15, %label_0
  %3 = call i64 @llrb_insn_getlocal_level0(i64 %1, i64 3)
  call void @llrb_set_pc(i64 %1, i64 94225474387896)
  %opt_lt = call i64 @llrb_insn_opt_lt(i64 %3, i64 60000001)
  %RTEST_mask = and i64 %opt_lt, -9
  %RTEST = icmp ne i64 %RTEST_mask, 0
  br i1 %RTEST, label %label_15, label %label_34

label_34:                                         ; preds = %label_25
  call void @llrb_insn_trace(i64 %0, i64 %1, i32 16, i64 8)
  call void @llrb_set_pc(i64 %1, i64 94225474387960)
  call void @llrb_push_result(i64 %1, i64 8)
  ret i64 %1
}

上記のLLVM IRでcallされている関数は全てLLVM bitcodeにプリコンパイルされており、LLRBはJITコンパイル時にそれをリンクする。そのため以下のようなインライン化と最適化がLLVMによって行なわれる。

define i64 @llrb_exec(i64, i64) #0 {
  ...

land.lhs.true.i:                                  ; preds = %label_25
  %49 = load %struct.rb_vm_struct*, %struct.rb_vm_struct** @ruby_current_vm, align 8, !dbg !3471, !tbaa !3472
  %arrayidx.i = getelementptr inbounds %struct.rb_vm_struct, %struct.rb_vm_struct* %49, i64 0, i32 39, i64 7, !dbg !3471
  %50 = load i16, i16* %arrayidx.i, align 2, !dbg !3471, !tbaa !3473
  %and2.i = and i16 %50, 1, !dbg !3471
  %tobool6.i = icmp eq i16 %and2.i, 0, !dbg !3471
  br i1 %tobool6.i, label %if.then.i, label %if.else11.i, !dbg !3475, !prof !3380

if.then.i:                                        ; preds = %land.lhs.true.i
  call void @llvm.dbg.value(metadata i64 %48, i64 0, metadata !2680, metadata !3361) #7, !dbg !3477
  call void @llvm.dbg.value(metadata i64 60000001, i64 0, metadata !2683, metadata !3361) #7, !dbg !3478
  %cmp7.i = icmp slt i64 %48, 60000001, !dbg !3479
  %..i = select i1 %cmp7.i, i64 20, i64 0, !dbg !3481
  br label %llrb_insn_opt_lt.exit

if.else11.i:                                      ; preds = %land.lhs.true.i, %label_25
  %call35.i = call i64 (i64, i64, i32, ...) @rb_funcall(i64 %48, i64 60, i32 1, i64 60000001) #7, !dbg !3483
  br label %llrb_insn_opt_lt.exit, !dbg !3486

llrb_insn_opt_lt.exit:                            ; preds = %if.then.i, %if.else11.i
  %retval.1.i = phi i64 [ %..i, %if.then.i ], [ %call35.i, %if.else11.i ]
  %RTEST_mask = and i64 %retval.1.i, -9
  %RTEST = icmp eq i64 %RTEST_mask, 0

  ...
}

いろいろインライン化されている上に僕の書いたCSSがクソなので読みづらいが、簡単に説明するとRubyVMの状態を取得し、<が再定義されているかどうかをチェックし、再定義されていなければicmp sltという命令が実行されるようになっている。インライン化されているので、llrb_insn_opt_ltという本来の関数の呼び出しのオーバーヘッドもなくなっている。 これはYARVのinsns.defの定義をほとんどそのまま持ってきてインライン化しているだけなので、実装コストが低いというメリットがある。

この最適化で、以下のようなベンチマークで、

ruby = Class.new
def ruby.script
  i = 0
  while i< 30_000_000
    i += 1
  end
end

llrb = Class.new
def llrb.script
  i = 0
  while i< 30_000_000
    i += 1
  end
end

LLRB::JIT.compile(llrb, :script)

Benchmark.ips do |x|
  x.report('Ruby') { ruby.script }
  x.report('LLRB') { llrb.script }
  x.compare!
end

以下のようにパフォーマンスが改善することがわかる。

# x86_64 GNU/Linux, Intel(R) Core(TM) i7-4790K CPU @ 4.00GHz
Calculating -------------------------------------
                Ruby      2.449  (± 0.0%) i/s -     13.000  in   5.308125s
                LLRB      8.533  (± 0.0%) i/s -     43.000  in   5.040016s

Comparison:
                LLRB:        8.5 i/s
                Ruby:        2.4 i/s - 3.48x  slower

LLRBの設計はどうなっているか

C拡張として作成

どうやらYARVは最初C拡張として作られていたらしい。JITコンパイラをC拡張として作るのは大分無理があると思ってたけど、YARVがC拡張でできるならJITコンパイラもC拡張でできるのではないかと思ったのでそうしてみた。実際、CRubyのforkとして開発をするのに比べると、コアと疎結合になるだけでなく、bundlerとかbenchmark-ipsとかその辺のgemをカジュアルに使えるので開発はやりやすい。

しかし実際のところ、基本的な演算子のメソッドが再定義されてるかどうかを持つ変数のシンボルが当然exportされてないことに気付き、そこでCRubyに変更を加えず開発することは諦めた。ので、k0kubun/rubyのllrbブランチにしかインストールできない。それでも、シンボルのexport以外には何もしないという縛りを設けてあるので、今後CRubyのバージョンが上がっても追従はしやすい気がする。

YARVに手を加えない保守的なアプローチ

YARVは大分長い間運用されている信頼性のあるVMなので、仮にJITコンパイラを導入したとしても移行期間中はベースになるYARVがそのまま使える状態の方が安心できると思っている。なので、LLRBはYARV ISeqからコンパイルする方式を取り、またYARVのコア自体には一切変更を加えないようになっている。

じゃあそれでどうやってJITを達成しているかというと、YARVにはopt_call_c_functionという命令があり、「ネイティブコンパイルしたメソッドを起動。」という説明が書いてある。これをJITに使わない理由はない。

というわけで、LLRBは前述した方法でCの関数ポインタを取得してそれをopt_call_c_functionオペランドとし、全てのメソッドを以下のようなISeqに変換する。

0000 opt_call_c_function
0002 leave

opt_call_c_functionYARVの内部例外を扱える *1ので、この命令を使えば大抵のことは実装できるし、少なくとも僕がテストを書いた範囲ではthrowとかbreakとかreturnが動いている。

しかし、ISeqを書き変える方法だと考慮しないといけないことがある。それは、YARVが内部例外を補足するcatch tableがプログラムカウンターの位置に依存して内部例外をハンドルすることである。 プログラムカウンターが0か2にいればすむようにcatch table自体を書き変えると分岐できなくなるケースがありそうだったので、LLRBはcatch tableには手を加えず、逆にネイティブコードの中でプログラムカウンターを従来通り動かす方を選択した。

なので、プログラムカウンターがそのような位置にあっても適切にleaveできるよう、余った位置にleave命令埋めをしている。これはshyouheiさんのDeoptimization Engineのnop埋めのパクリである。

正直それで問題が起きないのかはよくわかってないので他の人のツッコミ待ちだが、僕がテストを書いた範囲ではrescueとかensureが動いている。

サンプリングベースの軽量なプロファイラ

LLRBはプロファイラとしてサンプリングプロファイラを採用している。CPU時間で一定のインターバルおきにバックトレースのサンプリングを行ない、バックトレースの一番上に出現する頻度が高いものをコンパイル対象として選択する。

stackprofとかperfもサンプリングプロファイラであり、この手法自体は既に広く使われているため信頼性がある。

また、stackprofと同じくrb_postponed_job_register_one APIを使う + GC実行中かどうかを判定していて、おそらくコンパイルしても安全と思われる瞬間にコンパイルが行なわれる。

低いコンパイルコスト

上述したように、CRubyのC関数はビルドプロセスでLLVM bitcodeにプリコンパイルされているので、LLRBがJITコンパイルを実行する際ランタイムでCの関数のパースやコンパイルを行なうオーバーヘッドは発生しない。 また、RubyのASTからコンパイルするわけではなく、ある程度コンパイルされたYARV ISeqからコンパイルを開始しているので、おそらくこれが一番コンパイルコストが低い手法になると思われる。

TODOs

  • まだ全ての命令に対して最適化を実施できていないので、実際のアプリケーションで使うとパフォーマンスが劣化する。なので、まずはその対応から…
  • ISeqのインライン化に対応したい
  • まだコンパイルに対応してないYARV命令がいくつかある
    • expandarray, reverse, reput, defineclass, once, opt_call_c_function
  • コンパイル中に作るRubyのオブジェクトのうちGCを考慮できてない奴がある気がする

ビルド・使用方法

このへんを参照

気持ち

まだ大分未完成なんだけど、3か月くらい経ちモチベーションの維持が難しくなってきたので公開した。この辺で得られた知見をRubyKaigiとかで話せたらいいなと思っていたけど、まだ僕のCFPはacceptされていないので、まあ無理だったらRubyConfで会いましょう。

*1:YARV内部の例外についてはRuby under a microscopeとかに詳しく書いてあるのでここでは説明しない

Ruby コミッターになりました

m_sekiさんhsbtさんの推薦で、ERBのメンテナとしてRubyのコミット権をいただきました

以下が初コミットです。

github.com

普段テンプレート言語Hamlの高速化その更に高速な別実装Hamlitの実装をやっていてテンプレートエンジンの高速化に知見があり、ちょこちょこERBにも知見を還元したりしていたのですが、一昨日ふとERBの生成コードのiseqを眺めていた時に気付いてパッチを送った後、「入れるのめんどくさいし、ERBのコミッタやりますか」とお声がけいただいた形です。

というわけで、引き続き主に高速化の方面でERBのメンテナンスをやっていきますが、他にも以前僕がC拡張にしたHTMLエスケープとか、広い範囲でパフォーマンス改善をやれたらいいなと思います。

さっきのパッチに関連してto_sもメソッド呼び出しをバイパスできるようにしたら結構いろんなものが速くなるんじゃないかなと思ったんですが、ベンチを取ってみたところ効果が微妙だったので、コアの改善は難しいなあとか考えています。 *1

どうぞよろしくお願いいたします。

*1:前提として僕はメソッド探索をしなくなれば速くなると思っていて、メソッドキャッシュの影響はありそうなものの String#concat と String#to_s の呼び出しの間にどういう違いが発生するのかちゃんと調査していないのでわかっていない。

Treasure Data に入社しました

3月から Treasure Data で働いています。入社初日からタスクをアサインされ、RailsAPIの開発をやりました。

なぜ Treasure Data に転職したのか

前職もやりたいことができて優秀な同僚に囲まれ文句ない環境だったのですが、アルバイト入社から数えるともう3年半が経っていたし、入社前にイメージしていたような仕事も大体経験できていました。

そのままいても良かったのですが、ある程度の間隔で新しいことに挑戦しないと成長は止まってしまうと思っているので職場ごと変えることも考え始め、以下のような観点から Treasure Data に転職することに決めました。

  • エンジニアがユーザーになる仕事をしてみたい
    • 僕は開発者が使うツールを作るのが好きで、技術を売っている会社の方がそういうものを作る機会が増えそう
    • 正直あまりエンジニアリング以外に興味がないので、一般の人を対象にしたサービスを作っている会社より、エンジニアがユーザーな方が会社のビジネスに興味が持てる
  • 前職だとあまりやらなそうなことに挑戦する選択肢が増えそう *1
    • 分散システムやミドルウェアの開発、Javaを使ったバックエンドシステムの開発など
  • アメリカ (シリコンバレー) で働くチャンスを増やしたい*2
    • 上記を踏まえて今後のキャリアを考えると、日本にいるよりはアメリカにいた方が楽しい人生が送れそう*3なので、チャンスがあれば移住したい

その他、転職先に求めていたこと

市場調査のため転職ドラフトに登録していた時にいろんな企業様から声をかけていただいたきとてもありがたかったのですが、上記に加え下記のようなことを考慮し決めさせていただきました。

  • 自分が興味があったり得意な分野で尊敬できる人が多くいる
    • 今までは大体RubyRails、あとたまにインフラという感じで触っていて、そのどれでも自分よりすごい人がいる
  • 積極的にOSS活動や外部への発信をしている
    • 流石に全部オープンにするべきとは思ってないけど、何かすごい成果を出せたら社外に自慢する機会がないとモチベーションが維持しにくいと思っていて、そうしている人が多い会社の方がきっと楽しく仕事をしてるんだろうなと思う
  • 待遇が良い
    • 技術的な成長だけが人生じゃないし、総合的に見て幸せになるにはやっぱりお金も必要

気持ち

偉そうなことをいっぱい書いていますが、まだまだとても経験の浅い身ですので、早く成果を出して同僚の方に認めていただけるようがんばります。

*1:最初は僕がある程度得意であり人手も不足しているRailsアプリ(API)の開発をやります

*2:僕が働いているのは東京オフィスだけど、本社はMountain Viewにある

*3:技術で商売をしている会社は日本よりはシリコンバレーの方が多いよね、という話

Hamlを3倍速くした

Hamlコミッターになった

RubyKaigi 2015で「Hamlは遅いしメンテされてないので使わない方がいい」と言ったところ、じゃあ自分でメンテして速くしろということになりコミッターになった*1

当時から2年ごしなのは、当時のHamlのオーナーがあまりアクティブではなく最近a_matsudaさんがオーナーになったため。

HamlのTemple化・高速化を行った

Templeというのは、テンプレートエンジンをパイプライン的に構築するためのフレームワークで、テンプレートエンジン用の中間表現とその最適化エンジンを持つ。実装をTempleベースにすると、SlimHamlitに使われているような中間表現を使った最適化を適用しやすくなる。

コミット権をもらったので、RubyKaigi 2015でマージされないと言っていたパッチを自分でマージし、コード生成attributeのコンパイルをTemple化しながら高速化した*2。 それ以外にもいろいろ遅い原因を調べて改善した。

Hamlはどのくらい速くなったのか

Haml4 vs Haml5

つい昨日、それらの高速化を含むHaml 5.0.0.beta.2がリリースされた。

同じgem同士であるHaml4とHaml5を同時に比較するためのリポジトリを作っていて、これはslim-template/slimに入っているベンチマークからHTMLエスケープの回数が違う問題を修正したもの。

Ruby 2.4.0でHaml 4.0.7と現在のHamlのmasterを比較すると以下のようになる。 (Travisでの結果)

Rendering: /home/travis/build/k0kubun/haml_bench/templates/slim_bench.haml
Warming up --------------------------------------
          haml 4.0.7     1.595k i/100ms
   haml 5.0.0.beta.2     5.497k i/100ms
Calculating -------------------------------------
          haml 4.0.7     17.011k (± 9.9%) i/s -     84.535k in   5.031171s
   haml 5.0.0.beta.2     63.100k (± 1.8%) i/s -    318.826k in   5.054222s
Comparison:
   haml 5.0.0.beta.2:    63100.1 i/s
          haml 4.0.7:    17010.5 i/s - 3.71x  slower

というわけで、masterだとHaml4の3.71倍速くなっている*3。本当はmasterじゃなくて5.0.0.beta.2にしたいんだけど、あるバグ修正の際に誤って遅くしてしまったケースがあり、その対応を入れたリビジョンにしているので注意。5.0.0.beta.2だと3.28倍とかになる。

vs Slim, Faml, Hamlit, Erubi…

もともとslim-template/slimのベンチマークがテンプレート言語を超えて比較するベンチマークだったため、Slimなどとの比較もでき、以下のような結果になる*4。(Travisでの結果)

f:id:k0kubun:20170227151723p:plain:w540

まあまあ縮まったけどまだ遅いですねという感じ。やっていくぞ。

Erubiとは

これは完全に余談だけど、ErubiというERB実装をご存知だろうか。Railsで長らく使われていたErubisは、今年Erubiに置き替わった

Erubisはかなり前にメンテが止まっていた分frozen string literalが使われていなかったりして遅いんだけど、それがErubiでは解消され*5、ついうっかり僕もErubiのHTMLエスケープを速くしてしまったため、Hamlitとほぼ変わらないパフォーマンスが出るERB実装になっている。

どうやってHamlを速くしたのか

僕は自分のHaml実装を持っているので正直Haml使わないのに何故高速化をやっているかというと、何が原因でHamlが遅いのかに興味があったからである。というわけで、同じ興味を持っている人向けに、どうやって速くしたかを書いておく。高速化の手法自体はRubyKaigi 2015で話したので、以下では個別の話だけ書く。

以下はベンチマークの結果を見て効果があったと思われる順になっている。

1. attributeレンダリング用生成コードの最適化 haml/haml#904

このパッチが最も高速化に貢献しており、その分書くのも一番大変だった。簡単に言うと、以下のテンプレートは

.foo#bar{ class: 'baz', data: 'a' * 3 }

いままでは以下のようにコンパイルされていたが、

_hamlout.push_text(
  "<div#{
    _hamlout.attributes({"class"=>"foo", "id"=>"bar"}, nil,  class: 'baz', data: 'a' * 3 )
  }></div>\n",
  0,
  false,
)

以下のようにコンパイルされるようにした、ということである。

_hamlout.buffer << "<div class='baz foo'".freeze
_haml_attribute_compiler1 = 'a' * 3
case _haml_attribute_compiler1
when Hash
  _hamlout.buffer << _hamlout.attributes({ "data".freeze => _haml_attribute_compiler1 }, nil).to_s
when true
  _hamlout.buffer << " data".freeze
when false, nil
else
  _hamlout.buffer << " data='".freeze
  _hamlout.buffer << ::Haml::Helpers.html_escape(_haml_attribute_compiler1)
  _hamlout.buffer << "'".freeze
end
_hamlout.buffer << " id='bar'></div>\n".freeze

_hamlout.attributesって何、と思うかもしれないが、これはとにかく遅いメソッドであり、これを呼ばなくすると速くなる*6。コードの通り、動的な式('a' * 3の部分)の結果がHashじゃない限りはこの遅いメソッドが呼ばれず、また他の場所では可能な限り事前に連結された状態でバッファにconcatされるので、速い。

これをやるためには、ものすごく複雑なHaml attributeの仕様を正確に把握している必要があり大変だった。みんなもこの記事を5秒くらい見て欲しい。

このパッチで追加したHaml::AttributeCompilerは、互換性を一切崩さずに高速なコードを生成しようとしてものすごく色々なことを考えて書いてあるので語りたいことがたくさんあるんだけど、長くなるので何かのLTのネタとしてとっておく。

2. Haml::Bufferのオプション向けのオブジェクト生成を減らした haml/haml#897

Hamlレンダリングコードには必ず以下のコードが最初に入っていた。

_hamlout = @haml_buffer = Haml::Buffer.new(
  haml_buffer,
  {
    :autoclose=>["area", "base", "basefont", "br", "col", "command", "embed", "frame", "hr", "img", "input", "isindex", "keygen", "link", "menuitem", "meta", "param", "source", "track", "wbr"], 
    :preserve=>["textarea", "pre", "code"],
    :attr_wrapper=>"'",
    :ugly=>false,
    :format=>:html5,
    :encoding=>"UTF-8",
    :escape_html=>true,
    :escape_attrs=>true,
    :hyphenate_data_attrs=>true,
    :cdata=>false
  }
)

これが何を意味するかというと、レンダリングする度に毎回同じString 25個・Array 2個・Hash 1個を生成していることになる。毎回同じならこんなに渡す必要はないので、デフォルトとは違うオプションのみ渡されるようにした。そのため、普通に使っていれば、以下のようなコードが生成される。

_hamlout = @haml_buffer = Haml::Buffer.new(haml_buffer, {})

余計なオブジェクトが作られなくなるので速くなる。

3. attribute値のpreserveの無効化 haml/haml#903

以下のテンプレートは、

%p{ data: "foo\nbar" }

Haml4だと以下のようになるんだけど、

<p data='foo&#x000A;bar'></p>

Haml5では以下のようになるようにした。

<p data='foo
bar'></p>

これに関しては完全に仕様を変えているので全く褒められた改善ではないのだが、このpreserveと呼ばれる機能はとても遅い。遅いのは、ただでさえgsubが遅いのにそれ以外にもいろいろやっているからである。

SlimやFaml, Hamlitは後者の挙動で問題なく動いているし、入った時のコミットを見ても何で必要なのかよくわからん仕様なので、遅くなるデメリットの方が大きいと判断し削った。何か意見のある識者は声をかけてほしい。

4. 文字列リテラルのfreeze haml/haml#893

あまり解説する必要はなさそうだけど、静的な文字列をバッファに渡す時に必ず.freezeがついた状態で文字列を作るため、レンダリング時に文字列オブジェクトが生成しなくて済むというもの。

Temple化をすると静的な文字列は全て:staticという中間表現になるため、これに全部.freezeをつけていくのが簡単になる。

5. HTMLエスケープの高速化 haml/haml#902

Ruby 2.3でRuby本体のHTMLエスケープメソッドを高速化したので、gsubではなくそっちを活用するようにした。その際、古いHamlのものすごく複雑なエスケープの挙動が邪魔になるので、FamlやHamlitと同じごく普通の挙動にした。そこは後方互換性のない変更になってしまうが、流石にこれで困る人はいない気がしている。

github.com

HamlitよりHamlを使った方がよくなる?

ならない。Hamlitが速いのはHaml::Bufferが必要な機能を諦めているのとC拡張があるからで、少なくとも前者をやるのはHaml::Helpersを消せない都合かなり難しいし、後者はまだボトルネックではないのでやらなそう。

というわけで、Haml特有のヘルパーを使っている人などがHaml 5の想定ユーザーになるが、何らかの理由でFamlやHamlitへの移行を諦めた人には普通に嬉しいリリースなんじゃないだろうか。

僕も最終的にはみんなhaml.gemを使えばいい状態にはしたいが、そう簡単に後方互換性は切れるものじゃないと思うので、結構先になる気がする。

気持ち

FamlやHamlitではなくHamlを使っているアプリがある人はHaml 5.0.0.beta.2を試してみて欲しい。

*1:これは冗談で、a_matsudaさんからお誘いがあり、僕も興味があったのでやりますと言い、メンテをさせていただけることになった。

*2:余談だけど、HamlのTemple化をやりながらTempleを直していたところ、Templeの方のコミット権もいただいた。

*3:僕が変更を加える前のmasterは1.09倍くらい https://travis-ci.org/k0kubun/haml_bench/jobs/205696677

*4:Hamlは先ほどと同じリビジョンで、Erubiも現在のmasterである ad41891 にしてある

*5:なお遅かったのは素のErubisの話で、もともとActionViewはErubisを魔改造していたためRails上ではこの問題はなかった

*6:真面目に説明すると、改善後のコードがコンパイル時に済ませている文字列連結を全部レンダリング時にやっているのが_hamlout.attributesである

広告を非表示にする

Linux デスクトップ環境 2017

Linux デスクトップ環境 2016 - eagletmt's blogの人に影響を受けて自作PCでLinuxデスクトップを使い始めてから約1年半が経ち、僕の使う環境が一通り満足な状態になったので今どういう構成なのか書いておく。

僕はKeynoteを使う時とか会社のマシンでmacOSも割と使う都合、基本的に操作性がmacOSに近くなるようにしているので、macOSからLinuxに移行したい人の参考になるかもしれない。 *1

そもそも何故Linuxデスクトップを使っているのか

「苦労してmacOSに近づけるくらいなら最初からmacOS使えばいいじゃん」と言われそうだが、今この瞬間は大体以下の理由でLinuxデスクトップを使っている。

  • 趣味で作ったスペックが高めの自作PCmacOSが入れられない *2
  • 最新のmacOSではKarabinerが使えないが、Linuxでは自作のキーリマッパーが使える
    • Kababiner-Elements+keyhacに移行したけど、keyhacはたまにリマップされないことがあり困る
  • 仕事でサーバー用途にLinuxを使うことが多いのでLinuxに触る機会を増やしたい
    • パッケージ管理システムとか、strace, perfみたいにmacOSでは使えないツールの話
  • MacBookが高いので今後ノートPCを買う時にThinkpadとかにしたい

現在のLinuxデスクトップ環境

ディストリビューション: Ubuntu GNOME 16.04

去年はずっとArch Linuxを使っていたんだけど新年になってから気分転換にUbuntu GNOME 16.04を使っている。上述の「サーバー用途にLinuxを使う」勉強のため、サーバーやDockerによく使っているUbuntuにしている。

ArchはArchWikiが充実してるのと常に最新のパッケージが降ってくるのが便利だけど、パッケージをアップグレードするとたまに壊れる。Ubuntuはそこは安定しているイメージ*3だが、その分パッケージで普通に入るソフトウェアが基本的に古いので開発者にとっては多分Archの方が使ってて楽しい。

デスクトップ環境, ウィンドウマネージャ: GNOME, Mutter

普通のUbuntuだとデスクトップ環境としてUnityが入るんだけど、compizのウィンドウ切り替えがあまりにも遅くて苦痛だったのでGNOMEにした。ディストリビューションUbuntu GNOMEなのはそのため。

タイル型WMに興味があったのと動作が軽いので最初の半年くらいデスクトップ環境を使わずawesomeやxmonadを使っていたけど、僕はターミナルを半透明にしてTwitterとブラウザに重ねてVimとshellとTwitterとブラウザを同時に見るということをやっていて、ウィンドウを重ねるのがタイル型WMと相性が悪いのでやめた。

動作の軽さで言うとデスクトップ環境にxfceは試したことがあるが、僕は4KディスプレイをHiDPI(x2.0)で使っていて、HiDPIの対応がGNOMEに比べ微妙なので使っていない。GNOMEは綺麗かつまあまあ軽快なのでおすすめ。

使っているGNOME拡張 macOS風にするためにいくつかGNOME拡張をいれている。

  • Dash to dock: Dockを出しっぱなしにする
  • GNOME Shell Frippery: 日付と時刻を右端に出す
  • Panel osd: 通知の表示位置を右上にする

キーリマッパー: xmodmap, xremap

ErgoDox以外のキーボードを使う時にキー単位のリマップはxmodmapを使っていて、キーの組み合わせのリマップにはLinux向けの最強のキーリマッパーを作った - k0kubun's blogに書いたxremapを使っている。

xremapを使ってターミナル以外の場所でのEmacsバインディングを有効化*4していて、これがとても捗る。普通に設定するとデフォルトのC-a(全て選択)等が潰れてしまうが、macOSのCmd-aのようにAlt-aをC-aとして使っていて、大体macOSと同じように操作できる。xremapはDSLがシンプルなのでそういう複雑な設定を書きやすいのが利点だと思う。

ターミナル: rxvt-unicode

最初にrxvt-unicodeを選んでから他のを試していないけど、macOSでiTermを使っていた時に比べると高速に感じるし、特に困ることがないのでそのまま使っている。

日本語入力: ibus-skk

Linuxデスクトップを始めた時にuim-skkが何故か動かなくてworkaroundとしてibus-skkを選択しそのまま使っている。ibusはたまになんか困るような気がする*5けどまあAquaSKKとかと同じ感覚で普通に使える。

ファイラ, ビューア, スクリーンショット: Nautilus, eog, gnome-screenshot

特にこだわりがないのでGNOMEに標準添付のものを使っている。スクリーンショットImageMagickimport(1)を使うと画像に枠線が入ってしまうことがあるのでそれはやめた。

フォント: Ricty Diminished

ArchだとRictyを使っていたが、UbuntuだとRictyを入れるのが面倒なのでRicty Diminishedを使っている(雑)。4KディスプレイかつHiDPIだとフォントに関わらず文字が綺麗に見えるので金パワーは便利。

ブラウザ: Google Chrome

ほとんど説明不要だと思うけど、インストールが楽なchromium-browserを使っていないのは、「論理行単位の移動」を使うためにgtkrcのparagraph-endsを活用したくて.gtkrc-2.0が使える方を選んだため。

Twitterクライアント: Nocturn

LinuxだとYoruFukurouが使えないので、ElectronでYoruFukurou風のTwitterクライアントを作った - k0kubun's blog *6に書いたNocturnを使っている。自分好みに作ったので全OSで常用していて、そこそこメンテしている。

パスワードマネージャ: 1Password

他にLastPassとKeePass(X)を微妙に試したんだけど、今節約中なのでLastPassに課金したくなかった*7のと、KeePass(X)はHiDPI対応が微妙で1Passwordと比較して特にメリットがなかったので、結局使い慣れている1Passwordを渋々wineで使っている。インストールは面倒なので自動化してある

ブラウザ拡張も動くしDropboxでVaultを同期すればまあ普通に使えるが、挙動が遅めなのでLastPassへの移行も考えている。

音楽プレーヤ: Google Play Music

iTunesから移行した。たまたまiTunes Storeで買ってた曲が全てm4a(DRM free)だったので、曲のフォルダをまるごとアップロードするだけで移行が完了した。iTunes Matchと違って50,000曲まで曲のアップロードが無料だし、iOSアプリのクライアントも使いやすいので普通に便利だと思う。

気持ち

UbuntuとかGNOME使ってて大分ミーハーな感じだけど普通に使いやすい。MacBook高いしやめたいと思っていて、iOSアプリ作るみたいな用事もないみたいな人はLinuxデスクトップは試す価値があると思う。

*1:実際の設定手順は http://qiita.com/k0kubun/items/d2359ad51cf1cf783f4d に書いてメンテしている。自動化しない方が運用コストが低いものは手で入れている。

*2:スペックが高いので普段使いしたいが、自作している以上Appleのハードウェアではないので利用規約macOSを入れてはいけないという話 id:otituke

*3:Ubuntuを使ってる期間はまだ浅くて実際はどうなのか知らないので、あくまでイメージ

*4:ちなみに、C-aがC-Alt-aになっているのは、これを単にHomeにすると表示行移動になってしまうからで、gtkrcと連携して論理行移動できるようにしている dotfiles/.gtkrc-2.0 at 98aede78a0dc2c30f24d34e1452498d0d1f7fe85 · k0kubun/dotfiles · GitHub。これをgtkrcだけでやると、例えばC-kがGitHubに奪われるので、一度Altつきにリマップすることでそれを回避している。

*5:ちゃんと現象と原因を分析できてないので書くのは控えておく

*6:当時の記事と違い、今はデザインはオリジナルになっている

*7:1Passwordは既に買い切りのライセンスを持っている

2016年にやったこと

クックパッドで働くのは4年目、社会人としては2年目になった。2015年にやったことと同じフォーマットでまとめておく。

発表

今年は6本発表した。去年RubyKaigi前後にいろいろ集中してて死にかけたので、2か月に1回というのが僕にとってはちょうど良いペースだと思う。

RubyConf 2016

今年は海外のカンファレンスで登壇してきたというのが一番大きいと思う。英語は一応どうにかなったけど、うまい表現ができずもどかしいことがあるのでもうちょっとマシにしたい。あとこの成果で初めてクックパッドの業務にmrubyが導入されたように思う。

RubyKaigi 2016

100%クックパッドの業務時間で作ったOSSを題材に、今年は1人でRubyKaigiに登壇した。Barbeque自体はまだまだ改善点があるものの、ECSを活用してジョブ単位のオートスケールができ、マルチテナントで運用コストが低いシステムを作って実際に導入されるところまで持っていけたのはよかった。

Cookpad TechConf 2016

今年から弊社も社の技術カンファレンスをやるようになったので、アルバイト時代からずっとやりたいと思っていた仕事とその成果について話した。クックパッドの開発基盤グループにいると、難しい問題のデバッグや高速化にじっくり取り組めるのが楽しいと思う。

Rails Upgrade Casual Talks

大変ありがたいことに、発表しませんかというお誘いを初めていただいた。Rails 5が出る前だったので、cookpad.comのRails 4.1→4.2の話をした。

How to safely upgrade Rails // Speaker Deck

TokyuRuby会議10

Rails 5が出たのに合わせて、僕が送っていたパッチの紹介をした。東急は高速で喋っても誰にも怒られないのが楽しい。

My patches for Rails 5 // Speaker Deck

Roppongi.rb #2

toshimaruさんにお誘いいただいてMItamaeについて話した。何かOSSを作って、便利さを発表して、いろんな人に使ってもらう、という体験はとても楽しいので今後も続けたい。

MItamae Hacking Guide // Speaker Deck

かなりどうでもいい余談 今まで使った発表資料azusa colorsの色を全て使い切った。東急でやったけど、資料全体で赤とかを使うのは大分激しいのでちょっとやめた方がいいと思う。

ホッテントリ

ブログは1ヵ月に1記事くらい書くのを意識しているけど、はてなブログが11記事、Qiitaが6記事という感じだった。「発表」と重複があるけどブクマを貰えたのは以下の通り。

タイトル
1. 安心してRailsアップグレードを行うための工夫 - クックパッド開発者ブログ
2. Coffee, jQueryで書いていたElectronアプリをES6, React, Reduxで書き直した - k0kubun's blog
3. Linux向けの最強のキーリマッパーを作った - k0kubun's blog
4. Railsアプリ開発環境の高速化 // Speaker Deck
5. RubyなしでItamaeレシピを実行できる「itamae-go」を作った - k0kubun's blog
6. SSEを使ってHTMLエスケープを高速化してみた - k0kubun's blog
7. Itamaeのmruby実装「MItamae」が大体いい感じになった話 - k0kubun's blog
8. Scalable Job Queue System Built with Docker // Speaker Deck

いまだに400 users越えたことないので何かもっとバーンて感じの成果欲しい。

OSS活動

今年リリースしたOSS

Starだけ見ると去年に比べるとウーンという感じ。mitamaeを使ってくださっているみなさまありがとうございます。

Star リポジトリ
★65 k0kubun/mitamae
★64 cookpad/barbeque
★48 k0kubun/xkremap
★46 k0kubun/itamae-go

今年は↑でmruby, Docker, Go, Xlibあたりを触っていて、他にhescapeSIMDプログラミングで少し遊んだのと、もうちょっとC言語に強くなるためにCでCコンパイラを実装するのを途中までやり、そこでLLVMも触った。

今年活発にメンテしていた既存OSS

Star リポジトリ
★558 k0kubun/md2key
★407 k0kubun/Nocturn

md2keyは、ずっと欲しかったけど実装できなかったnested listが作れたのが大きい。あとプルリをいただいて、シーケンス図、表、発表者ノートなどがサポートされた。 Nocturnはデザインを変えReact/Redux化してから300くらいStarが増えた。既存の設計を真似たことでメンテが楽になった。

毎年新しいプロダクトをリリースしていきたいと思っているけど、既存のものを改善していくのも大切にしたい。

contribution

今年はmrubyを触り始めていろいろ踏んだのでmruby周りにいろいろ貢献した気がする。RubyとかRailsはもうそこまで不満がないけど、mrubyの方はまだまだやれることがたくさんありそう。mruby本体にも1回だけ貢献できた。

ポッドキャスト

timakinさんに誘われてboot.fmというポッドキャストを始めた。4話配信済み。2人でやっていても、ネタと収録する時間とゲストを呼ぶ人脈とコミュ力がなくてかなり大変で、miyagawaさんはすごい!!!!という気持ちになる。一方、自分がホストをやると、話したいことを自由に話せる楽しさがある。

bootfm.github.io

2017年は

  • Ruby以外で手になじむ言語を増やしたい
    • 今後もコードを書き続けたいが、このままだとRuby以外で仕事できなくなり失職しそう
  • 業務で使って困ることがないレベルまで英語の語彙や表現力を向上したい
    • ドキュメント読むのとOSS活動をする時に使うし、いつか海外でも働けるようにしたい
  • 執筆活動をしたい
    • というかやってるんだけど、ちゃんと出すところまで持っていけるようにしたい

私生活と両立して楽しくやれればいいなと思っています。来年もろしくお願いします。

広告を非表示にする

Linux向けの最強のキーリマッパーを作った

X Window Systemで動作するキーリマッパー「xremap」を作った

2017/1/9追記: xkremap→xremapにリネームしました

僕はKarabiner用のRuby DSLを作ったりそれを使って大量の設定を既述する程度にはKarabinerのヘビーユーザーなんだけど、デスクトップ環境にLinuxを使い始めてからもう1年以上経つ今でもLinux環境で使えるKarabiner並にリッチなキーリマッパーを見つけられずずっと不便していたので、ユースケースを満たす最低限のものを自分で作った。

github.com

ちなみにX用であって別にLinuxの何かに依存しているわけではないので、タイトルは釣りである。

これは何

RubyDSLかつシンプルなキーの指定方法によりキーリマップを設定することができる。例えば、以下の設定ファイルを書いてxremapに渡す*1とターミナルやEmacsの外でもEmacsライクなキーバインドが使えるようになる。

remap 'C-b', to: 'Left'
remap 'C-f', to: 'Right'
remap 'C-p', to: 'Up'
remap 'C-n', to: 'Down'

remap 'C-a', to: 'Home'
remap 'C-e', to: 'End'

remap 'C-k', to: ['Shift-End', 'Ctrl-x']

また、xbindkeys等のツールより優れている点はアプリケーション*2ごとのキーバインドを設定できるところにある。普段Emacsバインディングを使っていると、Slackが割りあてるC-kとコンフリクトするわけだけど、例えば以下のように書くとSlackでのみ下記の設定を適用し問題を回避することができる。

window class_only: 'slack' do
  remap 'Alt-n', to: 'Ctrl-k'
end

あと、任意のキーからシェルを起動することができるので、ランチャーとしても使える。

remap 'C-o', to: execute('nocturn')
remap 'C-u', to: execute('google-chrome-stable')
remap 'C-h', to: execute('urxvt')

補足だけど、ある単一のキーを別のキーにリマップするみたいなことはXmodmapとかでやったほうがいいと思う。その方が多分速いので。僕はErgoDoxのファームウェアでそういうリマップはできるので不要なんだけど、xremapはEmacsバインディングみたいな何らかのキーの組み合わせをキーの組み合わせやシーケンスに変換したい時に使う。

なぜ作ったのか

GitHub等でキーバインドが奪われるし、その度にJavaScriptを読みたくない

GitHubというサイトはC-k, C-bを奪ってくる。社員は誰もEmacsを使ったことがないに違いない。 僕はいままでEmacsバインディングinclude "/usr/share/themes/Emacs/gtk-2.0-key/gtkrc"によって設定していたんだけど、これだと普通にブラウザに奪われるため、user.jsを書く必要があって面倒だった。

xremapを使うとXのルートウィンドウレベルでキー入力イベントをフックしてリマップするので、例えばGitHubのコメント欄とかでブラウザにキーバインドが奪われない点が便利。

rbindkeysがRuby >= 2.2で動かない & wineを使うとSEGVする

類似のツールにrbindkeysというものがあり、というかほぼ要件を満たしていたので長い間使っていた。が、上述したようなランチャーの用途には使えず別のツールで解決していたのとか、単純に不安定なのとか、DSLが複雑などの理由でずっと自分向けの奴を作りたいと思っていた。仕様も内部実装も仕組みも全て僕の好みに変えたrbindkeysがxremapといえる。いままでお世話になりました。

rbindkeysと比較して、xremapは下記のような違いがある。

  • キーイベントの取得やリマップにLinux Input Subsystemを使わないので、Linuxじゃなくても(多分)動く & rootじゃなくても実行できる
  • キーボードを抜き差ししてもそのまま使える
  • DSLの違いにより、キーの指定がシンプルに書ける
  • C-kとかのリマップの挙動が安定している

あと、この場合別にRubyでもmrubyでもどっちでもいいなあとは思ったんだけど、最近慣れてきたmrubyを採用している点も異なる。

macOSと違ってCommandキーがないから色々コンフリクトするのを解決したい

コピーやペーストがC-c, C-vなどControlに割りあてられているため、例えばEmacsC-fとブラウザの検索のC-fが被ってしまっていた*3

アプリケーションごとにキーバインドを変えられる程度にはリッチかつ十分に安定したキーリマッパーがあると、macOSみたいにCommandキー相当のキーを作ることができる。会社で貸与されてるノートPCがMacBookなので今でもmacOSは使うのだけど、できれば複数の環境の差異をなくしたいというのがあった。

気持ち

デスクトップ環境でLinuxを使っている人はNocturnxremapをよろしくお願いします。

*1:systemdとかで起動するのがよい xremap/xremap.service at 6e8e1f21285ecedfa7ac88d703ad80d25a2699dd · k0kubun/xremap · GitHub

*2:正確にはウィンドウのclass

*3:gtkrcによる設定だと、テキスト入力欄かどうかで挙動が変わっていたが、検索とかは常に使えて欲しかった