JavaScriptの基本型とオブジェクト型のパフォーマンスの違い

JavaScriptの数値や文字列などの基本型はオブジェクトではありませんが、あたかも対応するクラスのインスタンスオブジェクトであるかのようにメソッドをコールすることができます。例えば、下記のプログラムではv1とv2はどちらも"123.00"という同じ文字列になります。x1は123という基本型の数値であり、オブジェクトではありません。一方、x2はNumberのインスタンスオブジェクトです。どちらも、Number.prototype.toPrecisionというメソッドが実行されます。

var x1 = 123;
var x2 = new Number(123);
var v1 = x1.toPrecision(5);
var v2 = v2.toPrecision(5);

今回の話は、この両者でパフォーマンスに違いはあるか、あるとしたらその理由は何か、ということです。

実際に確かめてみましょう。

function perf(x, n) {
  while (n-- > 0) {
    var v = x.toPrecision(5);
  }
}

n = 500000に設定して、perf()関数の実行時間を測定してみました。結果は私の手元にある遅いノートPCで以下のようになりました。単位はミリ秒です。

x Firefox 3.5.1 Chrome 3.0.193 Opera 9.64 Safari 4.02 IE 8.0
123 550 450 1482 1472 1182
new Number(123) 549 528 1275 1380 1151

ブラウザ毎の差はあっても、xが基本型のときとオブジェクトのときでは、それほど大きな差があるわけではありません。強いて言えば、OperaSafariではオブジェクトのときの方が高速だと言えるでしょう。

toPrecisionは組み込み関数ですが、ユーザ定義関数のときはどうなるでしょうか?現在の値に1を加えるというadd1関数を以下のように定義します。

Number.prototype.add1 = function () {
  return this + 1;
};

toPrecisionの場合と同様に、n = 500000に設定して測定してみました。結果は下記のように驚くべきものです。

x Firefox 3.5.1 Chrome 3.0.193 Opera 9.64 Safari 4.02 IE 8.0
123 750 12 1439 255 3345
new Number(123) 65 179 1112 150 1125

Chromeとそれ以外では、傾向が全く逆になることがわかります。Firefoxではオブジェクトの方が10倍も高速ですが、Chromeでは逆に基本型の方が10倍も高速です。基本型のときは、IEChromeの300倍近く遅いというのは笑ってしまいますが、それは置いておくとして、Chrome以外は、どうしてオブジェクトの方が高速なのでしょうか。直感的には基本型の方が高速な気がしますが、Chromeを除き事実は逆です。それを理解するため、ECMAScriptの規格書をみてみましょう。

現在のECMAScriptの規格書ECMA-262 Edition 3は非常にわかり難いのですが、メソッドコールに関する規則は、"11.2 Left-Hand-Side Expressions"に記載されています。x.add1()という単純な文法は、この記述に基づいて分解してみると、途中を端折ると次のような構造になります。

CallExpression => MemberExpression Arguments
               => MemberExpression . Identifier Arguments
               => MemberExpression [ Expression ] ()
               => x [ "add1" ] ()

x.add1 は x[ "add1" ] と同じです。この部分の評価は MemberExpression [ Expression ]をみると、以下のように書かれています。

The production MemberExpression : MemberExpression [ Expression ] is evaluated as follows:
1. Evaluate MemberExpression.
2. Call GetValue(Result(1)).
3. Evaluate Expression.
4. Call GetValue(Result(3)).
5. Call ToObject(Result(2)).
6. Call ToString(Result(4)).
7. Return a value of type Reference whose base object is Result(5) and whose property name is
Result(6).

1.と2.でMemberExpressionであるxの評価結果は数値の123になり、それが5.でToObject()されています。7.でそのオブジェクトに対して、プロパティを取得するようになっているので、結局、x.add1 という部分は ToObject(123).add1 になります。さらに、ToObjectは引数が数値のとき、ToNumberと同じですから、最終的に、

x.add1() => (new Number(123)).add1()

となります。

つまり、基本型に対して、メソッドコールを実行するということは、その基本型に対応するオブジェクトを一旦生成してから、そのオブジェクトに対してメソッドがコールされるというような意味になります。この一時的にオブジェクトが生成されるということが速度の違いとなって現れているものと思われます。しかし、これはあくまで意味論であり、この一時的に生成されるオブジェクトは外部から観測されないため、実装上の最適化が可能になります。toPrecisionのような組込み関数の場合には、その最適化がある程度考慮されていて速度差が少ないのかも知れません。ユーザ定義関数の場合には、Chrome以外は愚直に一時オブジェクトを生成しているもと思われます。ChromeのScriptエンジンV8はこのあたりの最適化が優れているのでしょう。

数値や文字列に対して、ユーザ定義関数を積極的に導入してNumberやStringを拡張するフレームワークが非常によく利用されるようになってきていますが、このあたりの事情を理解しておくことは、いろいろな面で役に立つと思います。