JavaScriptのswitch文の速度評価記事への補足

前回書いたエントリー「JavaScriptのswitch文の速度はブラウザの違いでこんなにも差があった。」に予想外にたくさんのブックマークがつき、貴重なコメントも頂きありがとうございます。それにインスパイアーされたので、ちょっと補足を書きました。個々の項目はブコメにヒントを得ましたが直接対応するものではありません。

どうしてこんなパフォーマンステストをしたの?

switch文のパフォーマンスを測定する動機になったのは、あるコードを読んでいたら、多量のswitch文があり、しかも、caseの数も多い。非常に見通しが悪い上に、これじゃ遅いだろうなと直感的に思いました。でも、ふと、switch文のcaseが定数なら分岐は高速に実行するようにコンパイルされるのではないか?JavaScriptのswitchはif-elseの並びと意味的には類似だけれど、caseが定数に限定されるなら、caseが多いから遅くなるというのは単なる思い込みで、実際に調査もしないでそんなこと言っていいのか、と思ったからです。

なので、目的はcase部が定数の場合には、swithc文のパフォーマンスはcaseの数に依存しないかどうかを知ることです。

でも、caseが1000個もあって、10万回もループするような非現実なコードのパフォーマンスなんて意味あるの?

caseの数に依存するかどうかを簡単に確認するのは、巨大なswitch文を書いて実際に要する時間を測定するのが手っ取り早い方法です。しかし、caseの数が1万や10万と大きすぎると、ファイルをロードするのにも時間がかかるし、さらに問題なのは一つの関数が巨大になり、そのことによるパフォーマンスの影響があるかも知れないと思いました。一方、caseの数が少ないと所要する時間が短すぎて誤差が多く、ループの回数を増やす必要があり、そうするとそのオーバヘッドが大きくなります。ちょうどの落としどころがcaseの数が1000で繰り返し回数が10万でした。

また、JavaScriptRubyなどのバイトコード処理系の実装も意識にありました。そのようなバイトコードインタープリタの骨格は大体下記のようなもので、バイト命令に応じてswitch文で分岐するものです。

while (true) {
  switch (次のバイトコード命令) {
  case OP_LOAD:  ...
  case OP_PUSH:  ...
  case OP_POP:   ...
  case OP_ADD:   ...
  case OP_MULT:  ...
  case OP_DIV:   ...
  ....
  }
}

このような実装では拡張性や柔軟性といったことより、高速性が求められます。switch文でベタに書くというのは必要悪な手段です。また、繰り返し回数も処理系が実行している間ずっとですから、10万回どころではなくもっとはるかに多くなると思います。

でも、JavaScriptでそんな言語処理系は書かないよね?

そうとも言い切れないという思いがします。JavaScriptは年々高速になっており、nativeコードとの差が殆どなくなれば、その可能性はいくらでもあると思っています。JavaScriptFlashプレイヤーを実装してしまうくらいの心躍る強者が現れることを密かに願っています。

caseの数の違いて言ってもcaseの数が1000個で固定じゃん?全然caseの数のパラメータになっていなじゃない。caseの数がもっと少ないときはどうなの?

caseの数自体をパラメータにするのは、ちょっと面倒なので、caseの数が100のときとの比較をしてみました。前回のときのcase数が1000で100番目のcaseが一致するときとの違いは以下のようになります。

v=100 Firefox Chrome Safari Opera IE
caseが1000個 12 47 5 420 930
caseが100個 12 47 4 422 922

1つのサンプルですが、case自体の数による有意差はないということが予想されます。

switch文を使うのってそもそも良くない設計でしょ?オブジェクト指向の本に書いてあったよ。ボクはswitch文は使わないから関係ないな。

確かにある場面ではその通りです。その場合switch文が良くないのは、オブジェクトの種類や状態によってswitch文で処理の場合分けするというロジックです。このような場合、1つのオブジェクトに対する処理がコード上の複数の場所に分散してしまい、オブジェクトの種類や状態が増えたとき、あちこちのコードを修正しなければならなくなります。そして、それはそもそもオブジェクト指向ではなくなります。似たようなswitch とcaseがあちこちに書かれている場合には設計を疑えというのは全くもってその通りです。

ただ、もっとプリミティブなの、オブジェクトにするには重すぎるもの、他と完全に独立したもの、などはその限りではなく、そこは臨機応変に適度なバランスということになります。

ハッシュマップでディスパッチしたときと比較するとどうなるの?

switch文のパフォーマンス測定の目的とは外れますが興味深い比較です。

switch文での場合と同等のことを行う下記のプログラムで測定してみました。

function perf() {
  var p = 0;
  var tab = {};
  for (var i = 1; i <= 1000; i++) {
    var key = i;                // ---- (1)
    tab[key] = function () { p++; };
  }
  var n = 100000;
  var tm1 = (new Date).getTime();
  while (n-- > 0) {
    var fn = tab[500];          // ---- (2)
    fn.call();
  }
  var tm2 = (new Date).getTime();
  alert("tm=" + (tm2 - tm1));
}

オブジェクトのハッシュを使っているので (2)のところのキーの500は1〜1000の間の値ならいくつでもよく、それは速度に依存しないはずです。(1)のkeyが数値のときと文字列のときを測定してみました。keyが文字列のときは、(1)は
var key = "s" + i
で、(2)は
var fn = tab["s500"]
です。

キー Firefox Chrome Safari Opera IE
数値 20 5 50 94 204
文字列 5 7 22 100 210

switch文のときとは全然異なる結果となりました。パフォーマンスが重要でキーが定数のときはswitch文を使うより、ハッシュマップを使った方が遙かに効率が良いということですね。

ブラウザの優劣をこんな極端なベンチマークで計るのは良くないし、誤解を与えるよ。

ベンチマークの目的はブラウザの優劣を評価することではなく、caseが定数とのきのswitch文のパフォーマンス評価ですが、結果的にブラウザの評価と捉えられた可能性があったことは確かです。その配慮が必要でした。人間、不思議なもので、自分が普段使っているものを批判されると、本人にはあまり関係ないことでも、とても不愉快に感じたりしますね。

Operaのconstは実はconstじゃないんじゃないの?

あれ?そうなの?

const v = 123;
v = 456;
alaert("v=" + v);

あ、ホントですね。const宣言しても値を変更できてしまいます。さらに、Safariのconstもconstじゃありませんでした。測定したブラウザの中ではconstが有効なのはFirefoxChromeだけだけのようです。

constはECMA規格にはまだ含まれていないから何が正しいということは言えませんが、const宣言はJavaScriptの従来の仕様を維持しつつ全体的な仕様とうまく整合をとるのが難しいように思います。

IEが高速な例ってないかな?

遅い例はたくさんあるのですが、JavaScriptの純粋な言語レベルでの高速な例というのはなかなかの難問ですね。もし、見つかったらそのエントリーを書きたいと思います。


はい、以上で補足はおしまいです。