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

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

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

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

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

正規表現が前提の文字列マッチメソッド(高機能、低速)

正規表現を使う場合も、=~よりmatch?の方が高速かつ可読性が高まりますので、マッチするかどうかをチェックするだけならmatch?にしましょう。自分も、=~~が前だったか後だったか毎回忘れてしまいます😅。

match?は、以下の記事にあるRubyの特殊変数を更新しません。おそらくその分速いと思われます。

[Ruby] Kernelの特殊変数をできるだけ$記号なしで書いてみる

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

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

さらに、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?と大差ないようです。

ベンチマーク

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

関連記事

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

デザインも頼めるシステム開発会社をお探しならBPS株式会社までどうぞ 開発エンジニア積極採用中です! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833 コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。 これまでにRuby on Rails チュートリアル第2版の監修および半分程度を翻訳、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れて更新翻訳中。 かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。 実は最近Go言語が好きで、Goで書かれたRubyライクなGoby言語のメンテナーでもある。 仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

夏のTechRachoフェア2019

週刊Railsウォッチ

インフラ

ActiveSupport探訪シリーズ