[rails/rails] String#squishの正規表現最適化で約2倍の高速化を実現
背景
String#squishは、文字列内の連続する空白文字をスペース1文字に置換し、前後の空白を削除するActive Supportの便利なメソッドです。しかし、従来の実装では単一のスペース(" ")も正規表現にマッチしてしまい、テキスト中に頻出するこのパターンが不要な処理を引き起こしていました。
本PRでは、正規表現を改良することで、実際に置換が必要なケースのみをマッチさせ、約2倍の性能向上を達成しました。
技術的な変更内容
変更前の実装
def squish!
gsub!(/[[:space:]]+/, " ")
strip!
self
end
従来の正規表現 /[[:space:]]+/ は、1文字以上の連続する空白文字すべてにマッチしていました。これには通常のスペース1文字も含まれるため、置換不要な箇所でも gsub! が実行されていました。
変更後の実装
def squish!
# Search for two or more `[[:space:]]` OR a single
# [[:space:]] that isn't `" "`.
gsub!(/([[:space:]]{2,}|[[[:space:]]&&[^ ]])/, " ")
strip!
self
end
新しい正規表現 /([[:space:]]{2,}|[[[:space:]]&&[^ ]])/ は以下の2つのパターンのみにマッチします:
-
[[:space:]]{2,}: 2文字以上の連続する空白文字 -
[[[:space:]]&&[^ ]]: 通常のスペース以外の空白文字(タブ、改行など)
文字クラスの交差演算子 && を使用し、「空白文字クラスに属し、かつスペースではない文字」という条件を表現しています。
パフォーマンス測定結果
ベンチマークでは、400回繰り返した長い文字列(空白、改行、タブを含む)を対象に測定が行われました:
STRING = (" sdsd dfdsf dfdsf\n dfdsf dfdsf" * 400).freeze
結果:
baseline: 5,011.2 i/s (199.55 μs/i)
opt: 9,972.4 i/s (100.28 μs/i)
Comparison: 1.99x faster
約2倍の高速化を実現しました。これは実際の文字列内容に依存しますが、一般的なテキスト処理において大きな改善となります。
設計上の考慮点
当初の実装案では strip! を gsub! の前に実行することで、先頭・末尾の空白マッチを減らす試みがありました。しかし、[[:space:]] クラス全体を効率的に strip する方法がないため、この変更は取り下げられました。最終的な最適化は正規表現の改良のみに集中しています。
正規表現自体は複雑になりましたが、マッチ回数が劇的に減少したことで、全体としてのパフォーマンスは大幅に向上しました。
まとめ
頻出パターン(単一スペース)を正規表現から除外するという単純な改良により、実用的なパフォーマンス向上を実現しました。文字クラスの交差演算子を活用した正規表現の最適化事例として参考になります。