まあ、タイトルは若干釣りで、特定のユースケースにおいて3割程度の高速化が見込める、というだけです。
joker1007/curl_escape: This gem provides fast URL escape by libcurl.
以下、実装経緯。
Railsのurl_for
を辿っていくと、最終的にクエリパラメーターとして渡したハッシュやらArrayやらに対して、Object#to_query
を発行します。
このObject#to_query
の実装は、ほぼCGI.escape
の実行です。
やたらとlink_to
の数が多くて、クエリパラメーターをそれなりに渡している、というページを表示しようとすると、割とこの処理時間が馬鹿にならない感じになってきます。
ということで、CGI.escape
を早くすれば、ちょっとはマシになるんじゃないかと思いました。
しかし、CGI.escape
の実装をRubyのままで今以上早くするのはちょっと難しそうでした。
なので、いっそCで書いてしまえというSamSaffron/fast_blankと同じ発想でいくことにしました。
文字コードとか考慮してURLをエスケープするのはかなり辛そうだったので、実装はlibcurlに丸投げして、CGI.escape
の仕様に合わせて文字列を適当に調整して終わり、というお手軽gemなので、一晩で適当にでっち上げました。
C拡張のパワーでCGI.escape
と同等の処理が5倍強の速度で実行できるようになったので、後はモンキーパッチでCGI.escape
の実装をC拡張側に差し替えればオーケーです。
ベンチマーク結果は以下の様になってます。
require 'benchmark/ips'
require 'cgi'
$LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
require_relative './lib/curl_escape'
Benchmark.ips do |x|
x.config(time: 15, warmup: 5)
x.report("cgi_escape") { CGI.escape("'Stop!' said Fred") }
x.report("curl_escape") { CurlEscape.escape("'Stop!' said Fred") }
x.compare!
end
Calculating -------------------------------------
cgi_escape 13.791k i/100ms
curl_escape 52.251k i/100ms
-------------------------------------------------
cgi_escape 130.471k (±17.8%) i/s - 1.889M
curl_escape 832.501k (± 5.6%) i/s - 12.488M
Comparison:
curl_escape: 832501.4 i/s
cgi_escape: 130470.6 i/s - 6.38x slower
url_for
benchmark
irb(main):005:0> app.dashboard_app_path("hogehoge", {foo: "bar", hoge: "aa", aaa: "aaa", bbb: "bbb", hogehoge: "%&[12]"})
=> "/apps/hogehoge/dashboard?aaa=aaa&bbb=bbb&foo=bar&hoge=aa&hogehoge=%25%26%5B12%5D"
irb(main):006:0> Benchmark.ips do |x|
irb(main):007:1* x.config(time: 15, warmup: 5)
irb(main):008:1> x.report("url_for") { app.dashboard_app_path("hogehoge", {foo: "bar", hoge: "aa", aaa: "aaa", bbb: "bbb", hogehoge: "%&[12]"}) }
irb(main):009:1> end
Calculating -------------------------------------
url_for 922.000 i/100ms
-------------------------------------------------
url_for 9.038k (±19.5%) i/s - 129.080k
=> #<Benchmark::IPS::Report:0x007fe017c73a68 @entries=[#<Benchmark::IPS::Report::Entry:0x007fe01b53b9b8 @label="url_for", @microseconds=15008895.1587677, @iterations=129080, @ips=9037.936272156061, @ips_sd=1764, @measurement_cycle=922, @show_total_time=false>], @data=nil>
irb(main):010:0> require 'curl_escape/core_ext/cgi'
=> true
irb(main):011:0> app.dashboard_app_path("hogehoge", {foo: "bar", hoge: "aa", aaa: "aaa", bbb: "bbb", hogehoge: "%&[12]"})
=> "/apps/hogehoge/dashboard?aaa=aaa&bbb=bbb&foo=bar&hoge=aa&hogehoge=%25%26%5B12%5D"
irb(main):012:0>
irb(main):013:0* Benchmark.ips do |x|
irb(main):014:1* x.config(time: 15, warmup: 5)
irb(main):015:1> x.report("url_for") { app.dashboard_app_path("hogehoge", {foo: "bar", hoge: "aa", aaa: "aaa", bbb: "bbb", hogehoge: "%&[12]"}) }
irb(main):016:1> end
Calculating -------------------------------------
url_for 1.192k i/100ms
-------------------------------------------------
url_for 12.170k (±14.2%) i/s - 177.608k
=> #<Benchmark::IPS::Report:0x007fe01b4c8da0 @entries=[#<Benchmark::IPS::Report::Entry:0x007fe015863560 @label="url_for", @microseconds=15003959.655761719, @iterations=177608, @ips=12170.236970422775, @ips_sd=1723, @measurement_cycle=1192, @show_total_time=false>], @data=nil>
url_for
はクエリパラメーターがそこそこ渡されているなら30%ぐらい早くなります。
また、エスケープ対象の文字列が多い程、相対的に早くなります。
なので、クエリパラメーターの多い検索ファセットのlink_to
などには多少効果があると思います。
マトリックステーブルの各マスにクエリパラメーターがそこそこ乗ったlink_to
が存在するようなページだと、大体50ms〜70msぐらいレンダリングが早くなりました。
gem突っ込むだけで高速化できる手段としては悪くないような気がします。