🔗 正規表現は文字列メソッドより「遅い」
新しい話ではなくて恐縮です。
Rubyに限らず、一般に正規表現は言語の文字列マッチメソッドより低速になります。
複雑なパターンを調べたい場合は正規表現を使うことになりますが、特に「開始文字列」「終了文字列」とのマッチを単純にチェックするだけなら、String#start_with?
やString#end_with?
でマッチを取る方が可読性の上でも速度面からもおすすめです。
本記事ではtrue/falseを返す文字列マッチメソッドについてのみ言及していますが、文字列の取り出しや置換といった操作についても、専用メソッドの方が正規表現よりも一般に高速なので、「正規表現は次なる手段」と考えるようにしています。
🔗 文字列マッチ専用メソッド(単機能、高速)
String#start_with?
(Ruby リファレンスマニュアル)String#end_with?
(Ruby リファレンスマニュアル)String#include?
(Ruby リファレンスマニュアル)
これらの文字列マッチ専用メソッドには、一応正規表現を引数として与えることもできますが、速度面では文字列を引数として与える方が有利です。
さらに、String#start_with?
やString#end_with?
の引数には文字列を複数与えられます。
string = 'test_some_kind_of_long_file_name.rb'
string.start_with?('empty', 'void', 'test_') #=> true
string.end_with?('useless', 'missing', 'rb') #=> true
配列に入れた文字列もsplat演算子*
を使えば渡せます。
array = %w(empty void test_)
string = 'test_some_kind_of_long_file_name.rb'
string.start_with?(*array) #=> true
なおString#include?
は引数を1つしか渡せません😢。速度的にもString#match?
と大差ないようです。
🔗 正規表現が前提の文字列マッチメソッド(高機能、低速)
正規表現を使う場合も、=~
よりmatch?
の方が高速かつ可読性が高まりますので、マッチするかどうかをチェックするだけならmatch?
にしましょう。自分も、=~
の~
が前だったか後だったか毎回忘れてしまいます😅。
match?
は、以下の記事にあるRubyの特殊変数を更新しません。おそらくその分速いと思われます。
🔗 ベンチマーク
Ruby 2.6.5を使いました。なおRubocop 0.75.0にperformance copも併用してかけてみましたが、特に何も言われませんでした。
参考: fast-ruby
# frozen_string_literal: true
require 'benchmark/ips'
SLUG = 'test_some_kind_of_long_file_name.rb'
def slower
SLUG =~ /^test_/
end
def slow
SLUG.match?(/^test_/)
end
def fast_start
SLUG.start_with?('test_')
end
def fast_end
SLUG.end_with?('rb')
end
def fast_include
SLUG.include?('_long_')
end
Benchmark.ips do |x|
x.report('String#=~') { slower }
x.report('String#match?') { slow } if RUBY_VERSION >= '2.4.0'
x.report('String#start_with?') { fast_start }
x.report('String#end_with?') { fast_end }
x.report('String#include?') { fast_include }
x.compare!
end
🔗 結果
$ ruby start_string_checking_match_vs_start_with.rb
Warming up --------------------------------------
String#=~ 245.277k i/100ms
String#match? 404.641k i/100ms
String#start_with? 501.627k i/100ms
String#end_with? 490.709k i/100ms
String#include? 414.337k i/100ms
Calculating -------------------------------------
String#=~ 3.736M (± 2.4%) i/s - 18.886M in 5.057521s
String#match? 8.825M (± 3.0%) i/s - 44.106M in 5.003011s
String#start_with? 15.313M (± 2.1%) i/s - 76.749M in 5.014165s
String#end_with? 14.849M (± 3.7%) i/s - 74.588M in 5.031067s
String#include? 9.559M (± 3.4%) i/s - 48.063M in 5.034701s
Comparison:
String#start_with?: 15313477.8 i/s
String#end_with?: 14848814.2 i/s - same-ish: difference falls within error
String#include?: 9558764.0 i/s - 1.60x slower
String#match?: 8824832.6 i/s - 1.74x slower
String#=~: 3736485.1 i/s - 4.10x slower
🔗 追記(2021/12/28)
ちなみにRuby 3.1にはMatchData#match
とMatchData#match_length
が新たに追加されました。正規表現マッチ後にキャプチャグループの文字列や長さを取り出せます。
参考: class MatchData
(Ruby リファレンスマニュアル)
参考: Ruby 3.1 adds MatchData#match and MatchData#match_length | Saeloun Blog
更新情報