はじめに
こんにちは、wai-doiです。
Railsアプリケーションを開発していて、文字列の削除をするコードを書くことがあると思います。
例えば以下のようなコードです。
zip_code = '123-4567'
zip_code.gsub(/-/, '')
この例では String#gsub
を使いましたが、他のメソッドでも同様のことを実現することができます。
私はそのたびに、どの書き方が良いのか迷っていました。そこで今回は速度の観点でどのメソッドを使うのがよいのかを測定してみました。
実行環境
速度の測定には、簡単に処理速度を計測することができる benchmark-ips
を用いました。
github.com
今回の実行環境です。
- Ruby (3.0.0)
- Active Support (6.1.3.1)
- benchmark-ips (2.8.4)
測定その1
郵便番号からハイフン -
1 文字を削除するときの速度を測定します。以下の観点を比較してみることにしました。
郵便番号を表す文字列から -
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
において引数が正規表現の場合がとても遅くなってしまうことがわかりました。それは、文字列が長いと正規表現のマッチ処理のステップ数が多くなるため速度に影響してしまったと思われます。
まとめ
今回は任意の文字列を削除するメソッドの速度を測定してみました。今回の知見から速度の面と用途の面で適切なメソッドを選んでいきましょう。