LoginSignup
47
46

More than 5 years have passed since last update.

Railsのurl_forを30%高速化するgemを作った

Posted at

まあ、タイトルは若干釣りで、特定のユースケースにおいて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突っ込むだけで高速化できる手段としては悪くないような気がします。

47
46
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
47
46