Ruby 2.5 の改善を自慢したい

技術部でフルタイム Ruby コミッタをしている笹田です。最近ひさびさに Ruby のライブラリに pull request をしました(show valid break point lines #393)。

12/25 のクリスマスに、Ruby 2.5 が無事にリリースされました(Ruby 2.5.0 リリース)。関係各位の努力に感謝します。いろいろなバグ修正、いろいろな新機能、いろいろな性能改善があります(詳細は、上記リリースノート、もしくは Ruby のソースコードにある NEWS ファイルをご参照ください)ので、試して頂けると良いと思います。そういえば、私がクックパッドに入社して初めての Ruby リリースでした。

前回の techlife ブログ( Ruby の NODE を GC から卒業させた )で遠藤さんが

クックパッドからの主な貢献としては、「trace 命令の削除による高速化」「分岐・メソッドカバレッジの測定のサポート」などがあります。

と書いていました。

リリースノートより「trace 命令の削除による高速化」について引用します。

命令列中のすべての trace 命令を削除することで、5~10% の高速化を実現しました。trace 命令は TracePoint をサポートするために挿入されていましたが、ほとんどの場合、TracePoint は有効にされず、これらの命令は無用なオーバヘッドとなっていました。Ruby 2.5 では、trace 命令を用いる代わりに、動的書き換えを利用します。詳細は [Feature #14104] をご覧ください。

それから、同じくリリースノートに、ブロックパラメータに関する性能改善について書いてあります。

ブロックパラメータによるブロック渡し(例:def foo(&b); bar(&b); end)が、”Lazy Proc allocation” というテクニックを用いることで、Ruby 2.4 と比べて約3倍高速化しました。渡されたブロックを、さらに他のメソッドに渡したい場合、ブロックパラメータを利用する必要があります。しかし、ブロックパラメータは Proc オブジェクトの生成が必要であり、ブロック渡しのためにはこれが大きなオーバヘッドとなっていました。”Lazy Proc allocation” はこの問題を解決します。詳細は [Feature #14045] をご覧ください。

これらは私の仕事だったので、紹介文を書かせて頂きました。他と比べて長すぎますね。まぁ、迫力があっていいんじゃないでしょうか。具体的な数字があると、うれしいですしね。

本稿では、これらの機能をもう少し深掘りして、リリースノートやチケットでの議論では出てこない、普段、私がどんなことを考えながら開発しているのかをご紹介できればと思っています。また、これらの目立つ改善以外の、Ruby 2.5 のために私が行ってきた活動についてもご紹介します。

trace 命令の除去と命令の動的書き換え

まずは、リリースノートに書いてある「trace 命令の除去」についての話です。

何を実現したいのか

この新機能については、福岡Ruby会議02でのキーノート「Rubyにおけるトレース機構の刷新」でお話ししました。

www.slideshare.net

というか、このキーノートに間に合わせるために開発予定を調整しました(EDD: Event Driven Development)。

Ruby には TracePoint という機能があります(リファレンス。古くは set_trace_func)。何かあると、例えば行を越えると何かフックを実行する、ということに使います。例えばこんな感じ。

trace = TracePoint.new(:line) do |tp|
  p tp
end

trace.enable do
  x = 1
  y = 2
  z = x + y
end

は、TracePoint#enable のブロック内で TracePoint:line イベントごとに TracePoint#new に渡したブロックを実行します。そのため、出力は次のようなものになります。

#<TracePoint:line@t.rb:6>
#<TracePoint:line@t.rb:7>
#<TracePoint:line@t.rb:8>

この機能を実現するために、VM が実行する命令列(バイトコード)中に、trace 命令というものを、フックを実行する可能性があるところに沢山挿入しています。一番多いのは :line イベント用に、行が変わる度にこの trace 命令が挿入されています。つまり、5 行のメソッドには 5 つの trace 命令が入っています。

TracePoint って知ってますか?」と聞くと、多くの Rubyist は「知らない」と答えると思います。つまり、あんまり使われない機能なのですが、使われないと、trace 命令は単なるオーバヘッドにしかなりません。つまり、多くの場合、この命令は無駄なわけです。この無駄を排除するためのコンパイルオプション(Ruby のコンパイラはいくつかコンパイルオプションを受け取ります)を指定すれば、TracePoint は動かなくなるけどちょっと速くなる、ということができたのですが、そもそもコンパイルオプションが指定できることを知っている人はごく少数ですよね。

なお、Ruby 2.4 以前を利用しなければならず、Ruby プログラムを 1% でも高速化したい、という方は、プログラムの最初に RubyVM::InstructionSequence.compile_option = {trace_instruction: false} と書いておけば、trace 命令を利用しなくなります(が、TracePoint が利用できなくなるため、例えば byebug といったデバッガが利用できなくなります)。

どうやって高速化するのか:trace 命令を排除

そこで、TracePoint のために trace 命令を利用する、ということをやめました。代わりにどうするか。命令列の命令を書き換える、ということにしました。

実際の例を用いて説明します。

x=1
y=2
p x+y+3

このプログラムは、Ruby 2.4 では次のようにコンパイルされていました。

# Ruby 2.4
0000 trace            1     (   2)
0002 putobject        1
0004 setlocal         x, 0
0007 trace            1     (   3)
0009 putobject        2
0011 setlocal         y, 0
0014 trace            1     (   4)
0016 putself          
0017 getlocal         x, 0
0020 getlocal         y, 0
0023 send             :+
0027 putobject        3
0029 send             :+
0033 send             :p
0037 leave 

いくつか trace 命令が入っていることがわかります。これを、Ruby 2.5 では、

# Ruby 2.5
0000 putobject      1       (   2)[Li]
0002 setlocal       x, 0
0005 putobject      2       (   3)[Li]
0007 setlocal       y, 0
0010 putself                (   4)[Li]
0011 getlocal       x, 0
0014 getlocal       y, 0
0017 send           :+
0021 putobject      3
0023 send           :+
0027 send           :p
0031 leave 

このように trace 命令を排除した状態でコンパイルしておきます。trace 命令がないので、なんとなく速そう、という気分が伝わるんじゃないかと思います。伝わるといいな。

さて、TracePoint を利用した時です。有効にした瞬間、Ruby プロセス中に存在するすべての命令列を探し出して、必要な形に変換します。今回の場合、次のように変換されます。

# Ruby 2.5 / Trace on!
0000 trace_putobject 1    (   2)[Li]
0002 setlocal       x, 0
0005 trace_putobject 2    (   3)[Li]
0007 setlocal       y, 0
0010 trace_putself        (   4)[Li]
0011 getlocal       x, 0
0014 getlocal       y, 0
0017 send           :+
0021 putobject      3
0023 send           :+
0027 send           :p
0031 leave 

最初の putobjecttrace_putobject に変わったのが見て取れると思います。普通の putobjectTracePoint について何もしませんが、trace_putobject は、まず TracePoint についての処理を行ってから、従来の putobject の処理を行います。

この手法は、「TracePoint をオンにするタイミングで命令書き換えが起こるので、それが大きなオーバヘッドになる」という問題がありますが、そもそも TracePoint は使われないので、問題ないと判断しました。

検討した点、苦労したところ

この辺から、リリースノートに書いていない話になります。

なぜ今まで trace 命令を使っていたのか?

見ての通り、難しい話はないので、もっと前からやっておけよ、と言われるかもしれませんが、YARV 開発から10年以上たって、やっと入りました。

以前は、命令の書き換えをしないほうが、別言語への変換(例えば、C 言語への変換)がやりやすいかな、と思っていたからなのですが、最近、「結局そういうことやってないしなぁ」と思ったり(すみません)、現在 JIT コンパイラの導入の話が進んでいますが、「書き換えるなら一度 JIT コンパイル済みコードをキャンセルすればいい」という踏ん切りがついたためです。なら、どうせなら派手に書き換えるようにしてしまえ、と思い、このようにしてみました。

書き換えるに当たり、trace_ prefix 命令ではなく、trace 命令を動的に挿入する、という選択肢もありました(これが、最も互換性に優れた方法です)。ただ、命令を増やすと命令アドレスを変更しなければならず、若干面倒です。そのため、各命令の位置は変更しない、という選択をしました(そのため、プロトタイプは一晩で実現できました)。今後、もっとアグレッシブに命令書き換えを行うためには、命令位置変更にも対応しないといけないと思っています。

trace 命令を沢山入れると、TracePoint を有効にしない場合の速度劣化を気にしなければなりませんでしたが、これからはこのオーバヘッドが気にならなくなります。そのため、TracePoint 向けのイベントを追加できると思っています。例えば、定数やインスタンス変数をトレースしたり、メソッドを呼び出す caller 側をフックしたりすることができると思っています。

trace_ prefix 命令をいつ戻すのか

TracePoint を有効にしている間は trace_ prefix 命令が必要です。ですが、無効にしたタイミングで、TracePoint 向けの処理が不要になります。そのため、初期実装では、TracePoint がすべて不要になったタイミングで、同じようにすべての命令列を探し出して元に戻す処理を入れていました。これは、TracePoint をパタパタと on/off 繰り返すようなプログラムはないだろうな、という予測に基づく設計でした。上記福岡Ruby会議02で紹介した時には、このような設計をしていました。off にするときのオーバヘッドを軽減するための工夫も盛り込んでいます。ただ、ある程度のオーバヘッドは、やはり必要になります(具体的には、ヒープ上からすべての命令列を探し出す部分)。

しかし、一部のライブラリ(具体的に問題としてあがってきたのは power-assert)では、TracePoint の on/off が多く起こるため、問題になることがわかりました。そこで、結局一度 trace_ prefix 命令に変更すれば、その後 TracePoint を無効にしても、そのままにしておくようにしました。TracePoint 向けのチェックがついてしまい、trace 命令があったときと同じように、若干遅くなるのですが、TracePoint をちょっとだけ on にしてその後は利用しない、というシチュエーションは、あまりなさそうかな、と思い、最終的に「戻さない」とすることにしました。

非互換の対応

この変更にともない、バックトレースや TracePoint などで得られる行番号がずれる、という問題がありました。多少、変わっても、人間が見る分には問題ないだろう、と思っていたのですが、人間以外が見る、具体的にはデバッガ等で特定の行番号(例えば、end の位置の行番号)に依存した処理があったため、byebug という有名な Ruby 用デバッガで問題が起こる、ということがありました。

問題は修正できたのですが、この問題が発覚した(教えてもらった)のが 12/23 で、リリースの直前でした。久々にリリース直前にたくさんコーディングをして(例年は、リリース直前には怖くてコードをいじれません)、なんとか問題ないところまでもっていくことができました。本件でお世話になった関係各位に感謝いたします。

我々も、もっとちゃんと著名ライブラリはチェックしておかないとな、という反省をするとともに、RC1 リリースなどでちょっと試してもらえないかと読者の皆様にお願いするところです。

Lazy Proc allocation によるブロックパラメータを用いたブロック渡しの高速化

Lazy Proc allocation というテクニックを考えて、ブロックパラメータを用いたブロック渡しを約3倍高速化することができました。

何を実現したいのか

あるメソッドに渡されたブロックを、ほかのメソッドに渡したい、というシチュエーションが時々あると思います。

def block_yield
  yield
end

def block_pass &b
  # do something
  block_yield(&b)
end

block_pass のようなメソッドを書くと思いますが、このときブロックローカル変数 b でブロックを受け取り、その受け取ったブロックを block_yield(&b) のように渡すことで、このような受け渡しを実現することができます(なお、ブロックを渡す他の(素直な)方法はありません)。

とりあえず、これで一件落着なのですが、ブロックローカル変数を使うと、yield するだけに比べて遅くなってしまう、という問題が生じます。というのも、ブロックローカル変数は Proc オブジェクトを受け取るのですが、この Proc オブジェクトの生成が重いためです。なぜ遅いかを大雑把に言うと、関連するローカル変数領域などをメソッドフレームをたどって芋づる的にヒープに確保しなければならないためです(この理由をより深く理解するには、Rubyのしくみ Ruby Under a Microscope などをご参照ください)。

渡されたブロックという情報を他のメソッドに渡すために、ブロックパラメータを経由してしまうため、Proc という、より冗長なデータを受け取ってしまい、遅い、という問題です。これを解決したい。

ポイントは、ブロックの情報だけだったら軽い、というところです。

どうやって高速化をするか:Lazy Proc creation

block_yield(&b) のようにブロックの情報を渡すだけなら、Proc は必要ありません。なので、ブロックローカル変数が block_yield(&b) のように、他のメソッドにブロックを渡すだけであれば、Proc を作らなくてもよいようにすれば速くなりそうです。本当に Proc が必要になるまで Proc オブジェクトの生成を遅延する、だから Lazy Proc creation と名付けています。まぁ、ある意味自明な機能なのですが、それでも名前を付けると、なんかカッコいいですよね。なお、並列分散処理の分野で "Lazy task creation" という技法があります。あまり関係ないですけど、カッコいい手法なので興味があれば調べてみてください。

さて、ここで問題になるのは、「Proc が必要ないのか?」ということを知る必要があることです。

def sample1 &b
  block_yield(&b)
end

このプログラムは、bProc にする必要はありません。ブロックの情報のまま、他のメソッドに渡してやればいいからです。

def sample2 &b
  b
end

このプログラムは、bProc にする必要があります。呼び出し側が返値として Proc オブジェクトを期待する(かもしれない)からです。

def sample3 &b
  foo(b)
end

このプログラムも、bProc にする必要があります。foo を呼んだ先で Proc オブジェクトを期待する(かもしれない)からです。

こう見ると、block_yield(&b) のようにしか使っていなければ、b はブロック情報のままで良さそうです。では、次の例はどうでしょうか。

def sample4 &b
  get_b(binding)
end

一見すると、b は触っていないので、ブロック情報のままで良いような気がします。が、binding オブジェクトを用いると、そのバインディングを生成した箇所のローカル変数にアクセスすることができるので、get_b の定義を、

def get_b bind
  bind.local_variable_get(:b)
end

のようにすると、b の中身をアクセスすることができます。この場合、bsample4 の返値になるため、やはり Proc オブジェクトにしなければなりません。binding が出たら諦める、という方法もあるのですが、binding はメソッドなので、任意の名前にエイリアスをつけることができます。つまり、どんなメソッド呼び出しも、binding になる可能性があるのです。まぁ、ほぼそんなことは無いと思いますが。

どうやら、プログラムの字面を見て、「bProc オブジェクトにする必要は無い」と言い切るのは難しそうです(このようなことを調べることを、コンパイラの用語ではエスケープ解析ということがあります)。

そこで、実行時に bblock_yield(&b) のようなブロック渡し以外のアクセスがあったとき、初めて Proc オブジェクトを生成するようにしました。

この高速化手法自体は長い間検討していたのですが、もう少し一般的なエスケープ解析が必要じゃないかと思って、それは Ruby では難しそうだな、どうしようかな、と考えていて実現できていませんでした。ただ、改めて考えてみると、ブロックパラメータへのアクセスを実行時に監視すればできることに、ふと自転車を乗っているときに気づいたので、実装することができました。

def iter_yield
  yield
end

def iter_pass &b
  iter_yield(&b)
end

def iter_yield_bp &b
  yield
end

def iter_call &b
  b.call
end

N = 10_000_000 # 10M

require 'benchmark'
Benchmark.bmbm(10){|x|
  x.report("yield"){
    N.times{
      iter_yield{}
    }
  }
  x.report("yield_bp"){
    N.times{
      iter_yield_bp{}
    }
  }
  x.report("yield_pass"){
    N.times{
      iter_pass{}
    }
  }
  x.report("send_pass"){
    N.times{
      send(:iter_pass){}
    }
  }
  x.report("call"){
    N.times{
      iter_call{}
    }
  }
}

__END__

ruby 2.5.0dev (2017-10-24 trunk 60392) [x86_64-linux]
                 user     system      total        real
yield        0.634891   0.000000   0.634891 (  0.634518)
yield_bp     2.770929   0.000008   2.770937 (  2.769743)
yield_pass   3.047114   0.000000   3.047114 (  3.046895)
send_pass    3.322597   0.000002   3.322599 (  3.323657)
call         3.144668   0.000000   3.144668 (  3.143812)

modified
                 user     system      total        real
yield        0.582620   0.000000   0.582620 (  0.582526)
yield_bp     0.731068   0.000000   0.731068 (  0.730315)
yield_pass   0.926866   0.000000   0.926866 (  0.926902)
send_pass    1.110110   0.000000   1.110110 (  1.109579)
call         2.891364   0.000000   2.891364 (  2.890716)

ベンチマーク結果を見ると、ブロック渡しをしているケースでは、修正前と後で3倍程度性能向上していることがわかります。

なぜ block.call は速くならないのか?

def block_call &b
  b.call
  # b.call と同じことをやるように見える yield なら速い。
end

このようなプログラムがあったとき、b がブロック情報のままでも yield 相当の処理に変換してしまえば、Proc オブジェクトを生成せずに済みそうな気がします。が、Proc#callyield には違いがあり、単純に yield に変換することはできません。

さて、何が違うかというと、$SAFE の設定、待避を行う、という機能です。yield では $SAFE について特に何もしませんが、Proc#call では、$SAFEProc オブジェクト生成時のものに設定し、呼び出しから戻すときに、呼び出し時の $SAFE に戻します。つまり、Proc 呼び出しの中で $SAFE を変更しても、呼び出しが終われば元通り、ということです。この違いがなければ、単純な yield に変換することは容易なのですが...。

ところで、$SAFE ってそもそもご存じですかね? 知らない方もいらっしゃるかと思いますが、これからも知らないでもあまり困らないのではないでしょうか。外部からの入力を用いて system メソッドなどで外部コマンドを呼ぶ、といった危険な機能を検知するかどうかを決めるための機能ですが、現在ではあまり利用するということは聞きません(危険なことができないようにするには、もっと OS レベルのセキュリティ機能を使うことを検討してください)。

そういうわけで、「あまり使って無さそうだから、$SAFE なくしませんか? 性能向上も阻害するし」、といったことを Ruby 開発者会議という毎月行っている Ruby コミッタの集まりで聞いてみたところ、まつもとゆきひろさんから、「$SAFE をなくすのはどうかと思うが、Proc オブジェクトで $SAFE の復帰・待避はしなくていいよ」という言質を取ったので([Feature #14250])、Ruby 2.6 では b.call のようにブロックパラメータを呼び出す速度が向上するのではないかと思います。だいたい、上記マイクロベンチマークの処理では、callyield と同じくらいの速度になるんじゃないかと思います。実際、Ruby コミッタ(パッチモンスター)の中田さん実験ではそれくらいの速度が出ているようです。

その他の貢献の話

さて、実はここからが本番なのです。が、もう長々と書いてしまったので、短くまとめます。

上記二つの性能向上は、良い数字が出ているので目立つのですが、実はあんまり苦労していません。だいたい数日で実現できてしまっています(その後、安定させるために、もう少し時間がかかっているんですが)。では、それ以外は何をしていたのでしょうか。

クックパッドに入って、Ruby のテスト環境を新たに整備しました([ruby-core:81043] rapid CI service for MRI trunk)。いわゆる普通のテストを定期的に行う CI は rubyci というものがあるのですが、結果が出るまで時間がかかる、通知がないなど不満がありました。そこで、最短で2分で結果が出る環境を整備することにしました。計算機はクラウド環境では無く、実機を利用しています。私が主催した東京Ruby会議11の予備費を用いて購入したマシンと、ある企業様から Ruby Association へ寄贈頂いたマシン、それからクックパッドで確保できたマシンを利用しています。マシン調達・運用にお世話になった/なっている皆様に深く感謝いたします。

テストは、コミットフックを用いるのではなく、とにかく何度も何度もテストを繰り返す、という方針をとっており、時々しか出ないタイミング問題などをあぶり出すことも挑戦することができました(普通は、同じテストを2度以上実行しても、結果は変わらないと思いますが、Ruby のテストですと、そうでもないことがあります)。実際、いくつかの問題を発見しています(多くはテストの不備でした)。また、結果を Slack に流して(普通ですね)、問題のあるコミットがあれば、すぐに気づくことができるようにしました。複数環境で実行しているため、たとえばビルドエラーが起こると Slack に数十の通知が一斉に飛んでくるので、とても焦るので直さないと、という気分になります。

それから、Ruby でコルーチンを実現するための Fiber 周りの整理・改善を行いました(リファレンスマニュアル)。結果は Fiber の切り替えが数% 速くなる、というなんとも地味な結果になりました。詳細は Ruby会議2017 での私の発表をご参照ください。

www.slideshare.net

実は、今年の多くの開発時間が、この改善につぎ込まれています。というのも、Fiber という機能を私が作ったのが約10年前なのですが、その頃テキトーにしていたところを、全部見直して書き直す、という作業になったからです。「実行コンテキストの管理」という、バグも出やすいムズカシイ部分ですし、そもそも覚えていない。そういう点でも気合いと開発時間が必要でした。うまくいかなくて、何度も最初からやり直しました。

この修正は、一義的には Fiber のための改善なのですが、実は狙いは将来導入を検討している新しい並行・並列のための機能を入れるための基盤作りでした。将来のデザインのために、今のうちに改善を行ったということになります。今年この点を頑張ったおかげで、来年は挑戦しやすくなったなという感触を持っています。

また、今年最大の Ruby への貢献といえば、遠藤さんにクックパッドにジョインして頂き、フルタイム Ruby コミッタとして活躍して頂いたことじゃないかと思います。遠藤さんによる目覚ましい成果は、だいたい私の成果と言っても過言ではないかと思います(過言です)。

おわりに

本稿では、Ruby 2.5.0 に導入した性能向上の仕組みについて、詳しくご紹介しました。その際、どのようなことを考え、気をつけながら開発をしているかも書いてみました。ちょっとだけのつもりが、長くなってしまいました。Ruby 2.5.0 と Ruby 開発の魅力を少しでもお伝えできていれば幸いです。来年も Ruby 2.6 そして Ruby 3 の実現のためにがんばります。

もし、Ruby インタプリタの開発が面白そうだな、と思ったら、できる限りサポートしますのでお声かけください。今年8月に好評だったRuby Hack Challenge( Cookpad Ruby Hack Challenge 開催報告 )の2回目である Ruby Hack Challenge #2 は1月末に英語メインで開催予定ですが(申し込み締め切りは過ぎてしまいました)、2月にも日本語メインで開催しようと思いますので、よろしければ参加をご検討ください。また、RHC もくもく会を、だいたい毎月どこかで開催していますので、そちらもチェック頂ければ幸いです。来月は 1/29 (月) に RHCもくもく会#4を開催予定です。

寒いのでお体に気をつけて、良いお年をお迎えください。

Ruby の NODE を GC から卒業させた

こんにちは、技術部のフルタイム Ruby コミッタの遠藤(@mametter)です。メリークリスマス。

本日 Ruby 2.5.0 がリリース予定です。いろいろな改善が含まれています。クックパッドからの主な貢献としては、「trace 命令の削除による高速化」「分岐・メソッドカバレッジの測定のサポート」などがあります。

ユーザから見える改善はいろいろと記事が出てくると思うので、この記事では、「抽象構文木のメモリ管理のリファクタリング」というあまりユーザから見えない改善を紹介してみます。

概要

Ruby のパーサは、NODE という内部的なオブジェクトで構成された抽象構文木を生成します。2.4 までの NODE は GC に管理される普通のオブジェクトでしたが、2.5 からは GC の外で管理するようになりました。これにより、3 つ嬉しいことがあります。

  • 大きなコードのパースが速くなりました。
  • NODE に詳細なコード位置情報(カラム情報)を載せることができました。
  • 将来的に NODE まわりのコードを整理できる準備ができました。

背景

NODE は、Ruby の抽象構文木のノードを表現する要素です。抽象構文木はソースコード文字列をプログラムで扱いやすい木構造として表現したものです。知らない人は拙著『Ruby でつくる Ruby』などを読んでください :-)

2.4 の NODE は GC に管理されるオブジェクトとして実装されていました。Ruby のオブジェクトは GC の制約で 5 ワード長と決まっています。そのうち 2 ワードは NODE の種別や行番号を表現するために予約されていて、自由に使えるのは 3 ワードのみでした。イメージ的には、長さが 3 で固定されている配列みたいなものです。

この方法には 3 つの問題がありました。

  • パース中に無駄な GC が起きる
  • 抽象構文木に詳細なコード位置情報を載せる余地がない
  • 抽象構文木の表現にムリ・ムダがある

パース中に無駄な GC が起きる

大きなコードをパースする際に NODE オブジェクトが大量に生成され、GC が走ってしまいます。しかも生成中の NODE は回収できないので、この GC はほぼ完全に無駄です。この現象は、a = 1 が大量に並ぶコードを実行すると観測できます。

図:eval("a = 1\n" * N) の実行時間(x 軸は行数 N 、y 軸は時間)

a = 1 が N 行並ぶコードの実行時間を、N を変えながら計測したものです。実行時間が非線形に増えていっているのがわかります。これはパース中に起きる GC のせいです。1 回の GC にかかる時間は O(N) 、GC が起きる回数は O(log N) なので、全体で O(N log N) の時間がかかります。

抽象構文木に詳細なコード位置情報を載せる余地がない

NODE には、その NODE に対応するコードの行番号だけが記録されていました。バックトレースの表示や行カバレッジの測定などでは、この情報だけで十分でした。

しかし、2.5 では分岐カバレッジをサポートすることになりました。分岐は同一の行の中で複数個現れることが普通にあるため、分岐カバレッジのレポートで「どの分岐の実行回数であるか」を示すために、カラム番号(行内で左から何番目か)が必要になりました。また、メソッドカバレッジのレポートでも、開始位置(def の位置)だけではなく終了位置(end の位置)もある方が便利でしょう。

しかし、大きさが限られている NODE には、これらの情報を載せるための場所がありませんでした。

抽象構文木の表現にムリ・ムダがある

3 ワード制限のために、Ruby の抽象構文木にはムリ・ムダが生じています。たとえば true を表すノード NODE_TRUE は、子ノードを持たないので、3 ワードがまるまるムダになってます。逆に、obj.attr += val を表すノード NODE_OP_ASGN2 は情報を 4 つ持つ *1 ので、2 つの NODE をカスケードさせて無理やり表現しています。

おまけ:昔は GC 管理する意味があったが、今はもう意味がない

Ruby 1.8 のころは、抽象構文木をトラバースする方式でインタプリタが実装されていました。このような実装では、抽象構文木が不要になるタイミングが自明ではないので、GC 管理に任せたい気持ちも理解できます。

しかし Ruby 1.9 では YARV が導入され、YARV コンパイラが抽象構文木をバイトコードに変換したあとは、抽象構文木はもう使われません。つまり、パースから実行開始までのわずかな期間だけのために、少なくない NODE オブジェクトを作って捨てることになります。世代別 GC が導入されたから実害はあまりないですが、ムダはムダです。

やったこと

NODE を GC 管理されるオブジェクトとしてではなく、ただの malloc されたバッファの中に確保するようにしました。大量に NODE を作っても、malloc バッファが増えるだけで GC のオブジェクトバッファは圧迫されないので、無意味な GC 起動は基本的に起きません。

主に大変だったのは次の 3 つです。

NODE が NODE 以外のオブジェクトを指すケースの検出と対応

NODE の子どもは基本的に NODE ですが、一部の NODE は NODE 以外のオブジェクトを指すことがあります。たとえばリテラルを表す NODE_LIT は、そのリテラルオブジェクト(文字列や配列など)を参照します。 NODE は GC 管理オブジェクトではないので、これらのオブジェクトはマークされません。そのままでは回収されてしまいます。そこで、NODE のバッファの他に、マークが必要なオブジェクトを管理する配列(mark_ary)を用意し、NODE が NODE 以外のオブジェクトを指すタイミングで mark_ary に追加するようにしました。*2

NODE を目的外使用しているコードの削除

NODE は抽象構文木のためのものなのに、「自動的に free される便利なデータ構造」として転用されてしまっていました。NODE_ALLOCA(自動的に free される一時的バッファ)と、NODE_HEREDOC(ヒアドキュメント関係のパーサの状態を管理するための一時的データ構造)で、いずれも抽象構文木の一部にはなりません。これらは imemo と言われる別種の内部オブジェクトに置き換えて対応しました。

Ripper 対応

Ripper は NODE が GC 管理オブジェクトであることを仮定して書かれているので、切り離しが大変でした。実は完全には切り離せておらず、NODE の先頭ワードはオブジェクトと同じ構造でないといけません *3 。これはいずれなんとかしたいと思っています。

結果

この改善により、背景に上げた 3 つの問題が解決しました(または解決のめどが立ちました)。

パース中の無駄な GC がなくなった

大きなコードの eval が線形になりました。

図:eval("a = 1\n" * N) の実行時間(x 軸は行数 N 、y 軸は時間)

グラフ的には圧倒的ですが、正直この改善が現実世界で生きてくることはあんまり期待できないと思っています。クックパッドの全ソースコードのパースで評価すると、2.67 秒が 2.60 秒になった程度でした。まあ、10,000,000 行のコードとか書きませんよね。コードを自動生成しているプロジェクトでは、ひょっとしたら役立つかもしれません。

笹田コメント:「おまけ:昔は GC 管理する意味があったが、今はもう意味がない」にあるように、Ruby 1.9 から NODE を GC 対象にする必要はないことはわかっており、ずっとやりたいと思ってペンディングしていたところ、遠藤さんが入社して一瞬で作ってくれました。ただ、当初は GC 回数が減るので、もっと性能インパクトがあるかと思ったんですが、現実的なコードでは影響がほとんどなく、意外でした。世代別 GC の性能が十分高い、ということだと思います。

NODE に範囲情報をもたせた

NODE が GC 管理から外れて自由に拡張できるようになりました。今までは各 NODE は開始行番号しか持っていませんでしたが、今は次の 4 つの情報を持っています。

  • 開始行番号
  • 開始カラム番号
  • 終端行番号
  • 終端カラム番号

分岐カバレッジ・メソッドカバレッジはこの情報にもとづいて測定結果を出力します。 また、カラム情報は他にも利用価値がありそうです。たとえば、NoMethodError が起きた箇所を行番号だけでなくカラム番号も出すことで、より詳細に位置を特定できるようにできるかも。

抽象構文木の表現のムリ・ムダを省いていける準備ができた

NODE が自由に使える領域は 3 ワードに限らなくなったので、今後はより柔軟に拡張できます。Ruby のソースコードのうち、評価器部分は YARV への置き換えで大きく整理されましたが、パーサ部分は未整理のまま拡張され続けてきていて、現在は Ruby の中でも最もわかりにくいソースコードの 1 つになっています。メンテナンスの観点でも、将来的に型システムを検討する土台としても、わかりやすくてメモリ効率的によいものになるように整理を進めたいと考えています。

まとめ

Ruby 2.5 NODE を GC 管理から外すことで、(1) パース時の無駄な GC を抑えた、(2) NODE の位置情報を詳細化した、(3) 抽象構文木の整理を進める土台を確立した、という改善を行いました。

謝辞:改良の方針や実装について弊社笹田とたくさん相談しました。また、bison を使って NODE の位置情報を実際に実装していくのは @yui-knk さんがやってくれました。ありがとうございます。

*1:レシーバ obj 、読み書きする attr 、演算子 + 、値 val 。

*2:mark_ary は YARV のコンパイラでも使われているテクニックです。

*3:RB_TYPE_P(obj, T_NODE) によって、NODE かそれ以外かを区別できないといけない。

料理教室のデザインリニューアルを支えた技術

料理教室事業部の長(@s_osa_)です。最近読んで面白かった漫画は『ランウェイで笑って』です。

クックパッド料理教室では今年10月にデザインの全面リニューアルを行ないました。

Before After
f:id:s_osa:20171219172624p:plain f:id:s_osa:20171219171826p:plain

ユーザー向けページの HTML, CSS, JavaScript を約1ヶ月でまるっと書き換えるプロジェクトでした。

今回はそんなデザインリニューアルを支えた仕組みについて書きたいと思います。

全面リニューアルの大変さ

「全面リニューアル」

聞いただけで大変さがにじみ出る言葉ですが、具体的に何が大変なのか少し考えてみます。

主な大変さは2つあると考えています。

スコープが大きい

デザインの全面リニューアルという性質上、全ページが対象になります。 クックパッド料理教室のコードはそれほど大きくない Rails ですが、それでも対象の view ファイルは約200ほどあります。

もちろん、これら200個のファイルだけを変更するわけではなく関連するファイルも同時に修正する必要があるため、実際の作業量はもっと大きくなります。

リリースブランチの長期間運用とビッグバンマージ

リニューアルにともなってデザインを大きく変更するため、全ページのデザインを一度に切り替える必要があります。 また、プロジェクトを進める一方で、バグ修正をはじめとして日常的にコードに変更を入れていく必要もあります。 これら2つの目的を果たすためにプロジェクトの期間中ずっとリリースブランチをメンテナンスしていく必要が生じます。

リリースブランチを長期間にわたってメンテナンスしていくのも大変ですが、その後、master にマージするのも大変です。 差分が大きくなればなるほど、バグが入り込む可能性は大きく、バグが起こったときの原因究明も難しくなります。

大変じゃない全面リニューアルを考えてみる

大変な理由がわかったところで、その大変さを取り除くことを考えます。

スコープをできるだけ小さくする

デザインの全面リニューアルである以上、すべての HTML, CSS, JavaScript, Image を書き換えることは避けがたいです。 しかし、それ以外の箇所は触らないようにしました。

「せっかくリニューアルするなら」と新機能の追加や機能改善をしたくなりますが、そこはグッと我慢して粛々と画面だけを書き換えます。 ただでさえ大きいスコープをさらに膨らませてリリースが遅れるくらいなら、可能な限り早くリリースした後に小さく扱いやすいスコープで機能追加や改善を行なうという方向性をプロジェクト開始時にチームで合意しました。

また、デザインの刷新だけでもユーザーにとっては大きな変更であり戸惑いが生じるので、機能については据え置くことで少しでも戸惑いを減らしたいという意図もありました。

リリースブランチをつくらない

身も蓋もないことを言ってしまうと、リリースブランチをなくせばリリースブランチの長期間運用もビッグバンマージも発生しません。 そこで、リリースブランチをつくるのはやめて、書いたコードは順次 master に入れるようにします。

しかし、先述のとおり全ページのデザインを一度に切り替える必要があるので、単純に既存の view を書き換えるという手段は使えません。

そこで、「全ページのデザインを一度に切り替える」と「順次 master にマージする」を両立するための仕組みをつくりました。

柔軟なデザイン切り替えを実現するために

つくりたい状況は

  • master に新旧2つのデザインが共存している
  • 2つのデザインを柔軟に切り替えられる

というものです。

上記2点を実現するための方法についてそれぞれ考えていきます。

master に新旧2つのデザインを共存させる

まず、master に新旧両方のデザインを共存させる方法を考えます。

プロジェクト期間中は一時的に共存期間が必要ですが、プロジェクト完了後は新しいデザインのみが使用され古いデザインが必要になることはありません。

そこで、古いデザインのためのファイルをまとめたディレクトリを作ります。 具体的には app/views, app/assets/images, app/assets/javascripts, app/assets/stylesheets の中に旧デザインのためのファイルを置くためのディレクトリを掘って、既存のファイルをそちらに移動し、プロジェクト完了後にまるっと削除できるようにします。

イメージとしては以下のようなディレクトリ構成になります。

# tree app
app
├── assets
│   ├── images
│   │   └── old
│   ├── javascripts
│   │   ├── application
│   │   │   └── foo.js
│   │   ├── application.js
│   │   ├── application_old
│   │   │   └── foo.js
│   │   └── application_old.js
│   └── stylesheets
│       ├── application
│       │   └── foo.scss
│       ├── application.scss
│       ├── application_old
│       │   └── foo.scss
│       └── application_old.scss
└── views
    ├── foos
    │   └── index.html.haml
    ├── layouts
    │   └── application.html.haml
    └── old
        ├── foos
        │   └── index.html.haml
        └── layouts
            └── application.html.haml

また、asset precompile 時に新旧両方の asset を作成するようにし、新旧それぞれの layout ファイルから読み分けるようにします。

# config/initializers/assets.rb
Rails.application.config.assets.precompile += %w(application application_old)
# app/views/layouts/application.html.haml
= stylesheet_link_tag 'application', media: 'all'
= javascript_include_tag 'application'

# app/views/layouts/application_old.html.haml
= stylesheet_link_tag 'application_old', media: 'all'
= javascript_include_tag 'application_old'

2つのデザインを柔軟に切り替える

ここまでで新旧両方のファイルを master に共存させることができました。

あとは render するテンプレートをいい感じに切り替えることさえできれば当初の目的を達成することができます。

Rails のテンプレート探索

テンプレートを望む通りに切り替えるためにはテンプレートがどのように探索されているかを知る必要があります。

Rails がどうやってテンプレートを探索しているかについては以下のエントリが詳しいです。

リンク先にあるようにテンプレート探索の仕組みは結構複雑なのでここでその詳細を解説することはしませんが、今回作ろうとしている仕組みは Rails がテンプレート探索に使用している resolver の仕組みを利用します。ここでは resolver についてのみ簡単に説明します。

Resolver

Rails が render するテンプレートを探索するために使用しているオブジェクトです。 現在のアプリケーションが持っている resolver の一覧は rails console で以下のメソッドを呼ぶことで確認できます。

ApplicationController.new.view_paths

メソッドの返り値は resolver が入った配列で、デフォルトでは以下のような resolver だけが入っています。

#<ActionView::OptimizedFileSystemResolver:0x007fe41c5d88a8
  @cache=#<ActionView::Resolver::Cache:0x7fe41c5d8bf0 keys=0 queries=0>,
  @path="/Users/shunsuke-osa/projects/cooking_school/app/views",
  @pattern=":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}">

何らかの view を追加するタイプの gem を使用している場合はその gem が提供する view を探索対象に含むための resolver が追加されているはずです。 *1

Path

resolver が持っている @path は文字通り探索対象のディレクトリを指し示す path です。デフォルトの resolver には app/views が指定されており、普段 Rails がこのディレクトリを対象にテンプレートの探索を行なっていることがわかります。

Pattern

resolver の @pattern からなんとなく察せるとおり、普段 Rails がやっている locale (e.g. ja, en), format (e.g. html, json), handler (e.g. haml, erb) に応じたテンプレートの切り替えも resolver によって行われています。 *2

パターン定義の中に含まれる :hoge はテンプレート探索で用いられる LookupContext において detail と呼ばれているもので、探索 path を動的に生成するために使用されます。現在使用されている detail の一覧は以下のメソッドで確認できます。

ActionView::LookupContext.registered_details
# => [:locale, :formats, :variants, :handlers]

また、パターン中に含まれる {} はブレース展開されます。

柔軟なテンプレート探索を実現する

Rails のテンプレート探索の仕組みを調べた結果、

  • Rails はテンプレート探索に使うための resolver を持っている
  • 適切な path や pattern を指定した resolver を追加すれば任意のテンプレートを render する仕組みをつくることができる

ということがわかりました。

方針が決まったので実装していきます。

シンプルなケース

view_paths に独自 resolver が追加されていない Rails に対してテンプレート探索の対象ディレクトリを追加するのは簡単です。 ActionView::ViewPaths が提供している prepend_view_pathappend_view_path を使用することで任意の @path を持った resolver を追加することができます。

# app/controllers/application_controller.rb
before_action :fallback_to_old_templates

private

def fallback_to_old_templates
  if prefer_new_template?
    append_view_path('app/views/old')
  else
    prepend_view_path('app/views/old')
  end
end

独自 resolver が使用されているケース

我々のアプリケーションでは jpmobile を使用していました。

jpmobile は resolver の pattern に :mobile という detail を追加して PC 向けとモバイル端末向けのテンプレートを切り替えています。 つまり、jpmobile が提供する端末ごとのテンプレート切り替えに対応しつつ、今回追加する新旧デザインの切り替えにも対応する必要があるため、前述の prepend_view_path, append_view_path に path を渡す方法では目的を果たすことができません。

そこで、jpmobile が提供する resolver を拡張した resolver を用意する必要があります。

つくりたい resolver は以下のようなものです。

  • jpmobile の提供する :mobile という detail に対応している
    • ':prefix/:action{_:mobile,}{.:locale,}{.:formats,}{+:variants,}{.:handlers,}'
  • 探索ディレクトリを柔軟に変更するための detail として :directories のようなものを持っている
    • '{:directories}:prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}'

つまり、両者を同時に満たすために '{:directories}:prefix/:action{_:mobile,}{.:locale,}{.:formats,}{+:variants,}{.:handlers,}' という pattern を持つ resolver である必要があります。

Detail

detail の追加は非常に簡単で、ActionView::LookupContext.register_detail を使用します。

# config/initializers/action_view.rb
ActionView::LookupContext.register_detail(:directories) { [] }

こうして detail を登録することによって、controller で self.lookup_context.directories= を呼んで、resolver の pattern にある :directories に値を渡すことができるようになります。

Pattern

本来であれば、jpmobile を拡張して pattern が '{:directories}:prefix/:action{_:mobile,}{.:locale,}{.:formats,}{+:variants,}{.:handlers,}' となる resolver を作成して prepend_view_path に渡すべきなのですが、

  • jpmobile が既存の resolver それぞれに対して resolver を作成しており、すべてに対応するのが面倒なわりにメリットが薄い
  • 今回はプロジェクト中のみ一時的に使用する

といった点を考慮し、jpmobile にモンキーパッチを当てることにしました。

# config/initializers/monkey_patches/jpmobile.rb
module Jpmobile
  class Resolver
    # Original: ':prefix/:action{_:mobile,}{.:locale,}{.:formats,}{+:variants,}{.:handlers,}'.freeze
    DEFAULT_PATTERN = '{:directories}:prefix/:action{_:mobile,}{.:locale,}{.:formats,}{+:variants,}{.:handlers,}'.freeze
  end
end

あまり褒められた方法ではありませんが、今回行ないたいのはデザインの全面リニューアルであり、そのための一時的な仕組みに対してあまり時間をかけたくなかったため割り切った判断をしました。

実際に切り替える

ここまで出来たらあとは実際に切り替えるだけです。

切り替え自体は controller で lookup_context.directories= を呼ぶだけなので非常に簡単です。

# app/controllers/application_controller.rb
before_action :set_view_template_directories

private

def set_view_template_directories
  case preferred_template # returns 'new' or 'old'
  when 'new'
    self.lookup_context.directories = ['', 'old/']
  when 'old'
    self.lookup_context.directories = ['old/', '']
  end
end

新旧どちらのテンプレートを優先するかを指定するメソッドを用意し、その返り値によってテンプレート探索の優先順位を切り替えています。

プロジェクト初期には新しいテンプレートは存在しません。しかし、その都度例外を吐かれると開発しにくいので、新テンプレートが見つからない場合には旧テンプレートにフォールバックするようにしています。

実際の手順

これまでの説明ではわかりやすさのため順番を前後させてきましたが、実際の作業手順としては以下のような順番でした。

  1. テンプレート切り替えの仕組みを実装する
    • preferred_template = 'old'
    • この時点では旧テンプレートが app/views にある
    • old -> new のフォールバックによってアプリケーションは正常に動き続ける
  2. 旧テンプレートや関連リソースを移動する
    • 旧テンプレートが app/views/old に移動
  3. 新テンプレートを実装していく
    • preferred_template = 'new' すると新テンプレートが優先的に render される
    • 新テンプレートが未実装のページは旧テンプレートにフォールバックする

いろいろな切り替え方

RAILS_ENV

一番わかりやすい切り替え方だと思います。

RAILS_ENV=development では新しいテンプレートを優先し、RAILS_ENV=production では古いテンプレートを優先するなどができます。

Query String

URL に ?template=new などを付加することによって、RAILS_ENV によるテンプレート指定を手軽に上書きする手段を提供します。production での確認などに利用していました。

Session

query string によるテンプレート指定は手軽で便利でしたが、ページ遷移を伴う場合に不便でした。そこで、プロジェクト後半にはページを遷移してもテンプレート指定が保たれるように session を用いたテンプレート指定も使用していました。 *3

外部データストアから設定を読み込み

全面リニューアルを実際にリリースする直前になると、リリース時に万一事故が起こったときの切り戻しを考えるようになりました。

しかし、我々のアプリケーションではデプロイやロールバックのために数分程度かかってしまいます。 つまり、リリース後にページが見れなくなるなどの問題が起こってしまった場合には数分間にわたってユーザーに迷惑がかかってしまいます。

そこで、デプロイなしでリリースするために外部のデータストアから指定するテンプレートを読み込めるようにしました。 Redis や memchached などの書き換えが容易なデータストアにテンプレート指定を保存し、リクエストごとに読み込むようにすることによって切り戻しにかかる時間を数秒程度まで短くすることができます。

パフォーマンスなど注意すべき点はありますが、比較的小規模なアプリケーションであることやリリース前後のみ使用するということを考慮して採用しました。

組み合わせると

それぞれのテンプレート指定方法を組み合わせて以下のような形で運用していました。

def preferred_template
  # preferred_template_by_* は 'new', 'old', nil のいずれかを返す
  # 優先順位を query, session, data store, env の順に設定
  preferred_template_by_query || preferred_template_by_session || preferred_template_by_configuration || preferred_template_by_env || 'old'
end

応用:段階的リリース

リクエストごとにテンプレート指定を柔軟に変更できるようになると「スタッフのアカウントに対して一足先に新デザインをリリースする」「ユーザー ID の末尾2桁が10以下のユーザーに対してのみ新デザインをリリースする」といったようにリリース対象を少しずつ広げるというようなことも可能になります。

おわりに

規模が大きくなることを避けられないデザインの全面リニューアルをスムーズに行なうために使用したテンプレートの柔軟な切り替え方法を紹介しました。

この方法を用いた結果、開発者以外のメンバーも含めて早い段階から production で新しいデザインを確認することができ、バグの早期発見に繋げることができました。 また、リリースまでに新しいデザインを触る時間を十分取れたため、リリース規模のわりには安心してリリースすることができましたし、事実としてもリリース後に大きな不具合は起こりませんでした。

ここまで触れませんでしたが、画面に関連しているものとしてテストがあります。しかし、古いテストを隔離してテストの中で指定するテンプレートを切り替えるという方針は同じです。

リリース後しばらくして問題なく動いていることを確認できたら、テンプレートを切り替えの仕組みを削除して新しいデザインだけを使用するようにした上で、はじめにつくった /old ディレクトリを rm -rf してリニューアル完了です。

影響範囲が大きいリニューアルをすることはあまり多くはないと思いますが、もし同じような状況に置かれている方の参考になれば幸いです。

*1:我々のアプリケーションでは kaminari, letter_opener_web などが含まれていました。

*2:デフォルトパターンは https://github.com/rails/rails/blob/6a902d43c76a8b5bc2ddd00b7c8af38f9fb82bdb/actionview/lib/action_view/template/resolver.rb#L209 で定義されています。

*3:切り替え方法とは別に session の書き換え方法を別途用意する必要があります

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/