パフォーマンスのためにJavaScriptのRegExpのcompileを使うべきか

  • 投稿日:
  • by

JavaScriptの正規表現のパフォーマンスに興味があったので調べてみました。 現時点での考察であり、JavaScriptの処理系による違いもありますし、将来的に最適化が進んだ場合は結果が変わることがあると思いますので、ご注意を。

まず、初めに見つけた記事が、これ。

http://stackoverflow.com/questions/9750338/dynamic-vs-inline-regexp-performance-in-javascript

簡単に言うと、

/[a-z]/.exec(str);

new RegExp('[a-z]').exec(str);

で、パフォーマンスが違うけど何で?という質問でした。 結論から言うと、これは等価ではありません。 前者は正規表現を一度しか評価しないのに対し、後者は毎回RegExpオブジェクトを作ります。

どうやら、次の3つ関数はほぼ等価のようです。

function func1(str) {
  return /[a-z]/.exec(str);
}

var re2 = /[a-z]/;
function func2(str) {
  return re2.exec(str);
}

var re3 = new RegExp('[a-z]');
function func3(str) {
  return re3.exec(str);
}

「ほぼ」というのは、実際に中身を見たわけではないからです。ただ、処理時間から推測しただけです。

さて、ここまでは理解できていたのですが、実はこの状態だと正規表現のコンパイルは行われていないようなのです。

どういうことかと言うと、

var re4 = new RegExp('[a-z]');
re4.compile();
function func4(str) {
  return re4.exec(str);
}

とすると、処理時間が短くなるケースがあるのです。 これはちょっと意外でした。正規表現のコンパイルというのは最適化の過程で勝手に行われるのかと思っていました。もしかしたら、最適化が行われるのはもっと長い時間処理が回っていないからなのかもしれませんが。

まずは、nodeでテストをしてみました。

https://gist.github.com/dai-shi/7394062

このベンチマークを走らせたら、一つの書き方だけ明らかに速くなりました。それが上記の書き方です。nodeのバージョンをv0.8, v0.10, v0.11と試しましたが、傾向はどれも同じです。

次にjsperfでも試しました。

http://jsperf.com/classname-check-regex-vs-indexof

ちょっと目的が違うのでindexOfとの比較が入っていますが、RegExpの方は大体同じです。 Chromeでの実行結果は、compile()を実行したケースがわずかに速くなりました。試している正規表現が違うので、コンパイルが効きにくいのかもしれません。 Firefoxでの実行結果も、compile()のケースが速くなりました。こちらは10%以上。しかも、全体的にChromeの実行結果より速い。ちょっと意外です。

さて、別のベンチマークも見てみましょう。

http://jsperf.com/javascript-compiled-regex/15

これは、globalマッチのテストケースも入っているのでちょっと分かりにくいですが、それを除けばやっぱりcompile()しているケースが速いです。Chromeで20%ほどアップ。Firefoxで30%ほどアップ。

ちなみに、globalマッチはChromeの場合は最速で、Firefoxの場合はもっとも遅いようです。これは使い方を悩みますね。

これらのベンチマーク結果を踏まえてどういう方針にするかですが、 まず、クライアントサイドのコードの場合は、10%~30%程度速くなることにどれくらい意味があるかですが、ばらつきもあるので、よほどパフォーマンスにシビアでなければ、inline型の正規表現でよいように思います。読みやすいといのが最大のメリットです。つまり、

/[a-z]/.exec(str);

こういう感じです。

一方、サーバサイドのコード、つまり、nodeの場合は、2倍近く速くなるケースもあり、また通常サーバサイドのコードはパフォーマンスを重視することから、現時点ではcompile型の正規表現を使うことに意味があるかもしれません。つまり、

var re = new RegExp('[a-z]');
re.compile();
function func() {
  return re.exec(str);
}

こういう感じです。関数の外で定義しないといけないので、場合によっては気をつける必要があります。変数名の衝突や、意図しない書き換えなど。

var func = (function() {
  var re = new RegExp('[a-z]');
  re.compile();
  return function() {
    return re.exec(str);
  };
})();

のようにする手もありますが、ますます読みにくくなっていきますね。

本来なら、re.compile()がなくてもコンパイルしてくれたらいいように思うのですが、コンパイルそのものにコストがかかるケースもあるので一回しか使わないような場合はコンパイルすべきじゃないということですね。JITなどの最適化はあまり詳しくないのですが、もしかしたら、次第にコンパイルされていくのかもしれません。

と、ここまで書いたところで見つけたのですが、

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Deprecated_and_obsolete_features

あれ、compile()は非推奨なのですね。ということは、やっぱり処理系が最適化の過程でコンパイルしていくのが筋ということですね。 非推奨のcompile()を使うとそれを強制的に動かせるのでベンチマークコードの場合は有利といったところでしょうか。

というわけで結論は、よほどの理由がなければ、inline型を使っておけばよいでしょう。new RegExp()を使ってもパフォーマンスが悪くなるわけではないので、場合によっては使ってもよいでしょう。

ぐるっと回って振り出しに戻った感じですが、個人的にはすっきりしました。