Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Ruby: 文字列マッチは正規表現より先に専用メソッドを使おう

更新情報

  • 2019/10/18: 初版公開
  • 2021/12/23: 更新

🔗 正規表現は文字列メソッドより「遅い」

新しい話ではなくて恐縮です。
Rubyに限らず、一般に正規表現は言語の文字列マッチメソッドより低速になります。

複雑なパターンを調べたい場合は正規表現を使うことになりますが、特に「開始文字列」「終了文字列」とのマッチを単純にチェックするだけなら、String#start_with?String#end_with?でマッチを取る方が可読性の上でも速度面からもおすすめです。

本記事ではtrue/falseを返す文字列マッチメソッドについてのみ言及していますが、文字列の取り出しや置換といった操作についても、専用メソッドの方が正規表現よりも一般に高速なので、「正規表現は次なる手段」と考えるようにしています。

🔗 文字列マッチ専用メソッド(単機能、高速)

これらの文字列マッチ専用メソッドには、一応正規表現を引数として与えることもできますが、速度面では文字列を引数として与える方が有利です。

さらに、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: Kernelの特殊変数を$記号を使わずに書く

🔗 ベンチマーク

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#matchMatchData#match_lengthが新たに追加されました。正規表現マッチ後にキャプチャグループの文字列や長さを取り出せます。

参考: class MatchData (Ruby リファレンスマニュアル)
参考: Ruby 3.1 adds MatchData#match and MatchData#match_length | Saeloun Blog

関連記事

はじめての正規表現とベストプラクティス1: 基本となる8つの正規表現


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。