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はどこかに非連続なものがあるようです。ある閾値でアルゴリズムを変えているのかも知れません。
一方、FirefoxとSafariは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; } } .... }
結果は以下の通りです。数値リテラルと同様にFirefoxとSafariは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個もあるようなコードは手では書かないにしても、IEはFirefoxより1万7千倍遅いというのはちょっと笑えます。
caseがvar変数の場合
caseがリテラルのときはFirefoxとSafariは最適化されていることがわかりましたが、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; } } }
結果は以下の通りです。定数リテラルのときはFirefoxとSafariは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が定数リテラルのときは、FirefoxとSafariはcaseの数に依存しないで高速に実行されます。caseがconst定数のときはFirefoxだけが高速に実行されます。さらに、caseの型が混在しても定数ならFirefoxは高速に実行されます。
今回のような巨大switch文が登場するようなコードを手で書くようなことはあまりありませんが、言語処理系などの実装でコードを自動生成したりするような場合には登場することがあります。テープルジャンプのディパッチ関数を個別に登録するより、switch文で1つにまとめた方が関数コールのオーバヘッドがなく高速に実行することできるため、C言語での実装にはよく用いられます。JavaScriptで同様のことをやる際には、JavaScriptのswith文のパフォーマンスを十分考慮する必要がありそうです。
追記:こちらのエントリーも合わせてどうぞ。