はじめに
こんにちは、wai-doiです。
Railsアプリケーションを開発していて、文字列の削除をするコードを書くことがあると思います。 例えば以下のようなコードです。
zip_code = '123-4567' zip_code.gsub(/-/, '') #=> 1234567
この例では String#gsub
を使いましたが、他のメソッドでも同様のことを実現することができます。
私はそのたびに、どの書き方が良いのか迷っていました。そこで今回は速度の観点でどのメソッドを使うのがよいのかを測定してみました。
実行環境
速度の測定には、簡単に処理速度を計測することができる benchmark-ips
を用いました。
今回の実行環境です。
- Ruby (3.0.0)
- Active Support (6.1.3.1)
- benchmark-ips (2.8.4)
測定その1
郵便番号からハイフン -
1 文字を削除するときの速度を測定します。以下の観点を比較してみることにしました。
- 比較するメソッドは
String#tr
,String#delete
,String#sub
,String#gsub
,String#remove
の 5 種類。 - 非破壊的と破壊的な変更をするメソッドはどちらが速いのか。
- 置換対象の引数に文字列でも正規表現でもとることができるメソッドはどちらが速いのか。
郵便番号を表す文字列から -
1文字を削除する Ruby のコードを実行します。String#remove
を使うため Active Support を require
しています。
require 'active_support/core_ext/string' require 'benchmark/ips' Benchmark.ips do |x| x.report('String#tr') { '123-4567'.tr('-', '') } x.report('String#tr!') { '123-4567'.tr!('-', '') } x.report('String#delete') { '123-4567'.delete('-') } x.report('String#delete!') { '123-4567'.delete!('-') } x.report('String#sub (string)') { '123-4567'.sub('-', '') } x.report('String#sub (regexp)') { '123-4567'.sub(/-/, '') } x.report('String#sub! (string)') { '123-4567'.sub!('-', '') } x.report('String#sub! (regexp)') { '123-4567'.sub!(/-/, '') } x.report('String#gsub (string)') { '123-4567'.gsub('-', '') } x.report('String#gsub (regexp)') { '123-4567'.gsub(/-/, '') } x.report('String#gsub! (string)') { '123-4567'.gsub!('-', '') } x.report('String#gsub! (regexp)') { '123-4567'.gsub!(/-/, '') } x.report('String#remove (string)') { '123-4567'.remove('-') } x.report('String#remove (regexp)') { '123-4567'.remove(/-/) } x.report('String#remove! (string)') { '123-4567'.remove!('-') } x.report('String#remove! (regexp)') { '123-4567'.remove!(/-/) } x.compare! end
benchmark-ips の出力は以下になりました。
Warming up -------------------------------------- String#tr 402.521k i/100ms String#tr! 434.153k i/100ms String#delete 453.843k i/100ms String#delete! 519.036k i/100ms String#sub (string) 245.331k i/100ms String#sub (regexp) 240.213k i/100ms String#sub! (string) 262.920k i/100ms String#sub! (regexp) 255.543k i/100ms String#gsub (string) 120.926k i/100ms String#gsub (regexp) 63.566k i/100ms String#gsub! (string) 117.623k i/100ms String#gsub! (regexp) 63.364k i/100ms String#remove (string) 56.069k i/100ms String#remove (regexp) 37.356k i/100ms String#remove! (string) 67.638k i/100ms String#remove! (regexp) 42.655k i/100ms Calculating ------------------------------------- String#tr 3.933M (± 2.4%) i/s - 19.724M in 5.018301s String#tr! 4.370M (± 1.6%) i/s - 22.142M in 5.067534s String#delete 4.514M (± 1.9%) i/s - 22.692M in 5.028565s String#delete! 5.081M (± 2.0%) i/s - 25.433M in 5.007540s String#sub (string) 2.419M (± 1.6%) i/s - 12.267M in 5.071963s String#sub (regexp) 2.385M (± 3.1%) i/s - 12.011M in 5.041188s String#sub! (string) 2.600M (± 3.5%) i/s - 13.146M in 5.062776s String#sub! (regexp) 2.539M (± 1.9%) i/s - 12.777M in 5.035136s String#gsub (string) 1.186M (± 2.6%) i/s - 5.925M in 5.001154s String#gsub (regexp) 630.950k (± 3.6%) i/s - 3.178M in 5.044408s String#gsub! (string) 1.074M (± 4.8%) i/s - 5.411M in 5.050392s String#gsub! (regexp) 528.834k (± 1.7%) i/s - 2.661M in 5.033851s String#remove (string) 487.230k (± 2.8%) i/s - 2.467M in 5.067225s String#remove (regexp) 349.516k (± 4.1%) i/s - 1.756M in 5.032162s String#remove! (string) 677.874k (± 1.7%) i/s - 3.450M in 5.090182s String#remove! (regexp) 427.461k (± 1.9%) i/s - 2.175M in 5.091096s Comparison: String#delete!: 5080887.5 i/s String#delete: 4514317.0 i/s - 1.13x (± 0.00) slower String#tr!: 4370478.5 i/s - 1.16x (± 0.00) slower String#tr: 3932740.8 i/s - 1.29x (± 0.00) slower String#sub! (string): 2600367.7 i/s - 1.95x (± 0.00) slower String#sub! (regexp): 2538594.2 i/s - 2.00x (± 0.00) slower String#sub (string): 2419134.9 i/s - 2.10x (± 0.00) slower String#sub (regexp): 2385105.6 i/s - 2.13x (± 0.00) slower String#gsub (string): 1185649.3 i/s - 4.29x (± 0.00) slower String#gsub! (string): 1073853.4 i/s - 4.73x (± 0.00) slower String#remove! (string): 677873.7 i/s - 7.50x (± 0.00) slower String#gsub (regexp): 630949.8 i/s - 8.05x (± 0.00) slower String#gsub! (regexp): 528833.6 i/s - 9.61x (± 0.00) slower String#remove (string): 487230.2 i/s - 10.43x (± 0.00) slower String#remove! (regexp): 427461.0 i/s - 11.89x (± 0.00) slower String#remove (regexp): 349516.3 i/s - 14.54x (± 0.00) slower
測定その 1 の考察
メソッドを速い順に並べると
String#delete
, String#tr
, String#sub
, String#gsub
, String#remove
の順番でした。用途がより限定的なメソッドは速く、より汎用的なメソッドは遅いという結果となりました。便利な String#gsub
をいつも使うのではなく、用途に合わせて適切なメソッドを使うことが良いということですね。
非破壊的と破壊的メソッドについては、常にどちらの方が速いかというのは今回の測定ではわかりませんでした。破壊的メソッドの方がオブジェクトを新たに作らないため速いのではないかと予想していましたが、速度のために破壊的メソッドを使うメリットはそれほど無いのかもしれません。
引数に文字列を与えるか正規表現を与えるかですが、常に文字列を与えたときの方が少しだけ速いことがわかりました。それは正規表現のマッチに少し時間がかかるためと考えられます。置換対象の引数が文字列で十分なときは文字列を使うのが良いですね。
また、とても学びになったのは String#remove
がとても遅かったということです。Rails で開発しているなら String#remove
がせっかく使えるから使おうと私はいままで考えていましたが、考え直す必要があるなと思いました。実装を見たところ、中で String#gsub!
を呼び出しているだけなので gsub
より遅いのは納得ですね。
測定その2
次に、レシーバーが長い文字列の場合の速度を測定してみました。
先ほどの郵便番号に対して1 万字の長さの文字列をくっつけた文字列をレシーバーにして、削除のメソッドを実行してみました 。比較する観点は、測定その 1 と同じにしています。
Benchmark.ips do |x| x.report('String#tr') { ('a' * 10000 + '123-4567').tr('-', '') } x.report('String#tr!') { ('a' * 10000 + '123-4567').tr!('-', '') } x.report('String#delete') { ('a' * 10000 + '123-4567').delete('-') } x.report('String#delete!') { ('a' * 10000 + '123-4567').delete!('-') } x.report('String#sub (string)') { ('a' * 10000 + '123-4567').sub('-', '') } x.report('String#sub (regexp)') { ('a' * 10000 + '123-4567').sub(/-/, '') } x.report('String#sub! (string)') { ('a' * 10000 + '123-4567').sub!('-', '') } x.report('String#sub! (regexp)') { ('a' * 10000 + '123-4567').sub!(/-/, '') } x.report('String#gsub (string)') { ('a' * 10000 + '123-4567').gsub('-', '') } x.report('String#gsub (regexp)') { ('a' * 10000 + '123-4567').gsub(/-/, '') } x.report('String#gsub! (string)') { ('a' * 10000 + '123-4567').gsub!('-', '') } x.report('String#gsub! (regexp)') { ('a' * 10000 + '123-4567').gsub!(/-/, '') } x.report('String#remove (string)') { ('a' * 10000 + '123-4567').remove('-') } x.report('String#remove (regexp)') { ('a' * 10000 + '123-4567').remove(/-/) } x.report('String#remove! (string)') { ('a' * 10000 + '123-4567').remove!('-') } x.report('String#remove! (regexp)') { ('a' * 10000 + '123-4567').remove!(/-/) } x.compare! end
Warming up -------------------------------------- String#tr 4.700k i/100ms String#tr! 5.329k i/100ms String#delete 4.399k i/100ms String#delete! 5.182k i/100ms String#sub (string) 15.998k i/100ms String#sub (regexp) 8.603k i/100ms String#sub! (string) 14.971k i/100ms String#sub! (regexp) 8.500k i/100ms String#gsub (string) 11.075k i/100ms String#gsub (regexp) 5.322k i/100ms String#gsub! (string) 13.001k i/100ms String#gsub! (regexp) 5.132k i/100ms String#remove (string) 10.418k i/100ms String#remove (regexp) 4.718k i/100ms String#remove! (string) 12.948k i/100ms String#remove! (regexp) 5.079k i/100ms Calculating ------------------------------------- String#tr 47.354k (± 3.1%) i/s - 239.700k in 5.066950s String#tr! 50.061k (±10.0%) i/s - 250.463k in 5.078335s String#delete 45.918k (± 6.5%) i/s - 228.748k in 5.005509s String#delete! 51.088k (± 5.2%) i/s - 259.100k in 5.088284s String#sub (string) 160.722k (±14.8%) i/s - 799.900k in 5.071508s String#sub (regexp) 83.337k (± 7.9%) i/s - 421.547k in 5.090275s String#sub! (string) 149.517k (±11.8%) i/s - 748.550k in 5.070937s String#sub! (regexp) 83.269k (± 5.1%) i/s - 416.500k in 5.015623s String#gsub (string) 140.134k (±10.0%) i/s - 697.725k in 5.024230s String#gsub (regexp) 55.118k (± 3.4%) i/s - 276.744k in 5.026910s String#gsub! (string) 137.273k (± 8.2%) i/s - 689.053k in 5.051651s String#gsub! (regexp) 54.317k (± 6.9%) i/s - 271.996k in 5.036091s String#remove (string) 99.842k (± 6.8%) i/s - 500.064k in 5.031706s String#remove (regexp) 45.780k (± 9.3%) i/s - 231.182k in 5.114578s String#remove! (string) 128.251k (± 8.8%) i/s - 647.400k in 5.085621s String#remove! (regexp) 53.039k (± 3.1%) i/s - 269.187k in 5.080040s Comparison: String#sub (string): 160722.0 i/s String#sub! (string): 149517.0 i/s - same-ish: difference falls within error String#gsub (string): 140134.1 i/s - same-ish: difference falls within error String#gsub! (string): 137273.0 i/s - same-ish: difference falls within error String#remove! (string): 128250.9 i/s - same-ish: difference falls within error String#remove (string): 99842.5 i/s - 1.61x (± 0.00) slower String#sub (regexp): 83337.3 i/s - 1.93x (± 0.00) slower String#sub! (regexp): 83269.4 i/s - 1.93x (± 0.00) slower String#gsub (regexp): 55118.3 i/s - 2.92x (± 0.00) slower String#gsub! (regexp): 54317.2 i/s - 2.96x (± 0.00) slower String#remove! (regexp): 53039.1 i/s - 3.03x (± 0.00) slower String#delete!: 51087.8 i/s - 3.15x (± 0.00) slower String#tr!: 50060.5 i/s - 3.21x (± 0.00) slower String#tr: 47353.9 i/s - 3.39x (± 0.00) slower String#delete: 45917.8 i/s - 3.50x (± 0.00) slower String#remove (regexp): 45779.8 i/s - 3.51x (± 0.00) slower
測定その 2 の考察
結果は測定その 1 から大きく変わり、長い文字列がレシーバーのときは String#delete
と String#tr
はとても遅くなってしまうことが分かりました。逆に String#sub
, String#gsub
, String#remove
の方が速くなっていました。
また、String#sub
, String#gsub
, String#remove
において引数が正規表現の場合がとても遅くなってしまうことがわかりました。それは、文字列が長いと正規表現のマッチ処理のステップ数が多くなるため速度に影響してしまったと思われます。
まとめ
今回は任意の文字列を削除するメソッドの速度を測定してみました。今回の知見から速度の面と用途の面で適切なメソッドを選んでいきましょう。