JavaScriptのswitch文の速度はブラウザの違いでこんなにも差があった。

はじめに

JavaScriptのswitch文は、CやJavaと異なりcaseのところに任意の式が書けるため、実行時にcaseの式も評価されるので基本的にはif-else文の並びと類似のものになります。つまり、caseの数に応じてパフォーマンスも低下すると予想されます。本当にそうなのか確認してみました。

測定した各ブラウザのバージョンは以下の通りです。

Firefox Chrome Safari Opera IE
3.5.6 4.0.249.30 4.0.4 (531.21.10) 10.10 8.0.6001

caseが数値リテラルの場合

パフォーマンスを測定するテストコードは下記のような簡単なものです。caseが1000個あるswitch文を10万回繰り返して実行したときの時間を測定しました。perf_test()関数の引数vに与える値に応じてcaseの条件で一致する場所が変わります。

function perf_test(v) {
  var p = 0;
  var n = 100000;
  var tm1 = (new Date).getTime();
  while (n-- > 0) {
    switch (v) {
    case 1: p++; break;
    case 2: p++; break;
    case 3: p++; break;
    ....
    case 1000; p++; break;
    }
  }
  var tm2 = (new Date).getTime();
  alert("tm=" + (tm2 - tm1));
}

各ブラウザごとにvの値に応じて測定した結果は以下の通りです。単位はミリ秒です。

v Firefox Chrome Safari Opera IE
10 12 5 4 50 109
100 12 47 5 420 930
1000 12 1940 4 4030 9150
-1 9 1935 4 4010 9100

caseが単純にif-elseの並び通りに実装されていれば、vの値に応じて時間がかかると予想されます。Chrome, Opera, IEでは確かにそのよう傾向が見られます。Chromeはどこかに非連続なものがあるようです。ある閾値アルゴリズムを変えているのかも知れません。

一方、FirefoxSafariはvの値に依存しません。CやJavaなどと同様にテーブルジャンプにコンパイルされているものと思われます。

caseが文字列リテラルの場合

次に、case部分が文字列リテラルのときを測定してみました。つまり、下記のようなコードです。

function perf_test(v) {
  ....
  while (n-- > 0) {
    switch (v) {
    case "s1": p++; break;
    case "s2": p++; break;
    case "s3": p++; break;
    ....
    case "s1000"; p++; break;
    }
  }
  ....
}

結果は以下の通りです。数値リテラルと同様にFirefoxSafariはcaseの数に依存しないようです。Chromeは今度は線形的に増加しました。

v Firefox Chrome Safari Opera IE
"s10" 2 22 25 74 406
"s100" 2 222 19 470 3710
"s1000" 2 2280 20 4690 34000
"sxxxx" 2 2280 27 4700 34500

Firefoxが数値リテラルより文字列リテラルの方が高速なのは意外です。いずれにせよ、一定時間で実行できるということはcase文の文字列を最小完全ハッシュ関数のようなもので実装しているのでしょう。

caseが1000個もあるようなコードは手では書かないにしても、IEFirefoxより1万7千倍遅いというのはちょっと笑えます。

caseがvar変数の場合

caseがリテラルのときはFirefoxSafariは最適化されていることがわかりましたが、caseが変数のときはそのような最適化はできないため、if-elseの並びと同様な実行になると予想されます。以下のようなコードで確認してみました。

var k1 = 1;
var k2 = 2;
var k3 = 3;
...
function perf_test(v) {
  while (n-- > 0) {
    switch (v) {
    case k1: p++; break;
    case k2: p++; break;
    case k3: p++; break;
    ...
    case k1000: p++; break;
    }
  }
}

結果は以下の通りです。予想通り、caseの数に応じて速度が低下するようです。IEのN/Aは「スクリプトの実行を中止しますか」のポップアップが表示されるため測定していません。Opera単純な線形増加のようですが、それ以外、特にFirefoxは大きく非線形のようです。

v Firefox Chrome Safari Opera IE
10 5 12 48 125 298
100 38 120 520 1230 N/A
1000 7830 4280 9200 11800 N/A
-1 7850 4230 9200 11820 N/A

caseがconst定数の場合

次に、caseにconst定数を書いた場合にはどうなるか測定してみました。const宣言は先日正式リリースされたECMAScript 5th editionでもまだ採用されていませんが、多くのブラウザでサポートされています。コードは以下の通りです。varの代わりにconstを書くだけです。

const k1 = 1;
const k2 = 2;
const k3 = 3;
...
function perf_test(v) {
  while (n-- > 0) {
    switch (v) {
    case k1: p++; break;
    case k2: p++; break;
    case k3: p++; break;
    ...
    case k1000: p++; break;
    }
  }
}

結果は以下の通りです。定数リテラルのときはFirefoxSafariはvの値によらず一定時間でしたが、const定数の場合はSafariは増加するようになりました。var変数との違いはなく、constによる最適化はされないようです。IEはconst宣言自体をサポートしてません。

v Firefox Chrome Safari Opera IE
10 12 16 48 125 N/A
100 12 118 516 1240 N/A
1000 12 4280 9250 11800 N/A
-1 9 4300 9190 11890 N/A

caseがvar変数とconst定数が混在する場合

さらに、varとconstを下記のように交互にした場合どうなるかを測定してみました。これはFirefoxのときのみ意味があるものです。

const k1 = 1;
var   k2 = 2;
const k3 = 3;
var   k4 = 4;

数値は省略しますが、結果は全部var変数のときと同じでした。交互ではなく、1つだけvarでそれ以外はconstの場合も全部がvarのときと同様でした。

型が混在する定数リテラルの場合

つまり、下記のようなコードの場合です。

function perf_test(v) {
  ....
  while (n-- > 0) {
    switch (v) {
    case       1: p++; break;
    case    's2': p++; break;
    case       3: p++; break;
    case    's4': p++; break;
    ....
    case     999: p++; break;
    case 's1000': p++; break;
    }
  }
  ...
}

結果は以下の通りです。同じ型の定数リテラルだけのときに比べて面白い違いが出ました。SafariのN/Aは「スクリプトの実行を中止しますか」のポップアップが表示されました。

v Firefox Chrome Safari Opera IE
9 2 7 53 78 160
"s10" 2 18 110 78 234
99 2 65 640 710 1740
"s100" 2 172 1060 500 1970
999 2 1260 10100 7080 16300
"s1000" 1 2880 N/A 4810 19200
-1 1 1240 10180 7100 16600
"sxxxx" 2 2780 N/A 7400 19200

Firefoxはすばらしいです。Safariは同じ型の定数リテラルのときは一定時間でしたが、型が混在するとその効果はないようです。IEは型で分類しているのでしょうか、時間が半分になっています。

まとめ

caseが定数リテラルのときは、FirefoxSafariはcaseの数に依存しないで高速に実行されます。caseがconst定数のときはFirefoxだけが高速に実行されます。さらに、caseの型が混在しても定数ならFirefoxは高速に実行されます。

今回のような巨大switch文が登場するようなコードを手で書くようなことはあまりありませんが、言語処理系などの実装でコードを自動生成したりするような場合には登場することがあります。テープルジャンプのディパッチ関数を個別に登録するより、switch文で1つにまとめた方が関数コールのオーバヘッドがなく高速に実行することできるため、C言語での実装にはよく用いられます。JavaScriptで同様のことをやる際には、JavaScriptのswith文のパフォーマンスを十分考慮する必要がありそうです。


追記:こちらのエントリーも合わせてどうぞ。