StrScanner は高速なスキャンを行うための拡張モジュールです。
Ruby レベルでは、文字列の先頭から順々にマッチを行っていこうとすると、どうしても 残りの文字列をあらためて生成してやらなければいけません。たとえば、ごく簡単な スキャナの例として次のようなものを書いたとします。
ATOM = /\A\w+/ SPACE = /\A[ \t]+/ while str.size > 0 do if ATOM === str then str = $' return $& elsif SPACE === str then str = $' return $& end end
これでも確かに目的を果たすことはできるのですが、この場合だと、「$'」を呼びだすごとに
マッチした後の部分の文字列が生成されてしまいます。もとの文字列が小さければよいのですが、
文字列が大きくなると急激にオーバーヘッドが増えていき、使いものにならないほどスピードが
おちてしまいます。プロファイルをとってみるとわかりますが、この増加分はほとんどすべて
メモリ割り当てで消費されています。
まあそれもあたりまえで、1トークンが平均5バイトくらいと仮定すると、スキャンが終わる
までのメモリの総使用量は (str.size * 2) + (str.size ** 2 / 5) で、2乗のオーダーで
増加するわけです(最初の str.size * 2 は元の文字列とトークンにしたものの合計)。
str.index( regexp, point ) でいけるかなーと思ったんですが、やってみるとこれは \A を うけつけてくれないので捨てました。もちろん \A を使わないでやってもいいんですが、 そうすると今度は先頭からマッチしているかどうか調べてやる必要がでてきますし、いちいち 文字列全体にマッチをくりかえすので、文字列が大きくなるとマッチのスピードが急激に低下して しまいます。
というわけで作ってみたのが StrScanner です。C レベルで文字列をいじって文字列の再生成を ポインタのインクリメントにおきかえているので、相当オーバーヘッドが少なくなります。 簡単に計測したところでは、2KBの文字列でも実測10倍は速くなりました。 文字列が大きくなるほどさらに差はひらいて、400KBで試すともう天と地ほどの違いがでます。
まず単純な例として、上に挙げたお手軽スキャンルーチンを書きかえてみます。 見やすいように、もう一度上の版もならべておきます。
お手軽バージョン
ATOM = /\A\w+/ SPACE = /\A[ \t]+/ while str.size > 0 do if ATOM === str then str = $' return $& elsif SPACE === str then str = $' return $& end end
StrScannerバージョン
ATOM = /\A\w+/ SPACE = /\A[ \t]+/ s = StrScanner.new( str ) while s.rest? do if temp = s.scan( ATOM ) then return temp elsif temp = s.scan( SPACE ) then return temp end end
まず、スキャンの前にStrScannerオブジェクトを生成します。
メソッド scan は引数の正規表現とのマッチをしてその部分文字列を返し、同時に
そのあとまでポインタを進めてくれます。上の例でのマッチ、再生成、文字列摘出が
まとめてできるわけです。まあ、たいして難しいところもないですね。
類似のメソッドとしては、ポインタだけすすめる skip、マッチしたかどうかだけを
知る match? があります。
ちなみに、マッチに使う正規表現は必ず文字列の先頭からマッチしなければいけません (ようは、 /\A…/ じゃないとだめってことです)。もし先頭より後でマッチした場合は、 マッチしたところまで全部がマッチしたものとみなされてしまいます。例えば次のような場合は
s = StrScanner.new( "word word" ) ret = s.scan( /\s+/ )
何事もなかったかのように ret には "word " が返ります。先頭からマッチしなかった場合、 それを知る手段はありません。skip、match? でも同様です。
dupしないと生成が高速になりますが、その場合もとの文字列からとりだしたポインタを
そのまま使うので、もしスキャン中にその文字列がガーベージコレクトされると落ちます。
また、他のスレッドがその文字列を触れるときも危険です。StrScannerでは最初に取得した
ポインタと長さを最後まで使うので、もし文字列が短かく変更されたり、realloc がおこったり
すると落ちます。
よほどの理由(文字列が100MBあるとか…)がない限り、おとなしく dup しておいたほうが
いいです。…てゆーか、そういう場合はそもそも別の手段を使うほうがいいですよね…