2011年12月17日

JavaScriptの無名関数の実行 (function(){})() と (function(){}()) の違い


JavaScript Advent Calendar 2011 (オレ標準コース) 17 日目、polygon_planet です。

ずっと Advent Calendar 参加してみたいなぁと思ってたんですが
ネタが思いつかない日々で半分諦めてたんですが、考え過ぎな気がしてきたので
別におもしろい記事でもないし、技術的にも参考になるのか不明ですがとりあえず書きます。

もしかしたら同じようなこと解説してる記事がすでにあるかも…(うまく検索できてない)


JavaScript で無名関数をその場で実行するとき、
  1. (function() {  
  2.   // 処理  
  3. })();  
という書き方が主流っぽいですが、
  1. (function() {  
  2.   // 処理  
  3. }());  
こう書いたらどう違うのか。

ちなみに、
  1. function() {  
  2.   
  3. }(); // エラー  
これは式ではなく文として扱われるためエラーです。
ただし、以下のように実行する場合はエラーではありません。
  1. var arr = [function(){}()];        // [undefined]  
  2. var obj = {foo: function(){}()};   // {foo: undefined}  

(function(){})() vs (function(){}())

どちらも同じように実行され、結果的には同じなので
とりあえず、無名関数が生成され実行するまでの JavaScript コードの過程を追ってみます。

今回は手元に SpiderMonkey に付いてる JavaScript shell しかなかったので、それを使います。
わりと前にコンパイルしたやつですが JavaScript バージョンは 1.8 なので現行と大差ないと思います。
中間コードは js shell の dis で逆アセンブルして確認します。

まず (function(){})() から。
$ js
js> dis(function() { (function(){})() });
flags: LAMBDA INTERPRETED
main:
00000: anonfunobj (function () {})
00003: group
00004: pushobj
00005: call 0
00008: pop
00009: stop

Source notes:
0: 5 [ 5] pcbase offset 5
anonfunobj というは、無名関数を定義していると思われますが、
確認のため Mozilla のソースコードを見てます。

今回は js shell のバージョンに合わせるため
/mozilla-central/ ではなく /mozilla/ を参照します。

jsopcode.tbl
  1. 305 /* Auto-clone (if needed due to re-parenting) and push an anonymous function. */  
  2. 306 OPDEF(JSOP_ANONFUNOBJ,  128, "anonfunobj",  NULL,     3,  0,  1, 19,  JOF_OBJECT)  
このあたりの JSOP_* から参照できます。
JSOP は jsopcode.c あたりで確認できます。
  1. 3777               case JSOP_ANONFUNOBJ:  
  2. 3778 #if JS_HAS_GENERATOR_EXPRS  
  3. 3779                 sn = js_GetSrcNote(jp->script, pc);  
  4. 3780                 if (sn && SN_TYPE(sn) == SRC_GENEXP) {  
  5. 3781                     JSScript *inner, *outer;  
  6. 3782                     void *mark;  
  7. 3783                     SprintStack ss2;  
  8. 3784   
  9. 3785                     LOAD_FUNCTION(0);  
  10. 3786                     inner = fun->u.i.script;  
  11. // ... (長いので省略) ...  
ここでは LOAD_FUNCTION が使われています。

JavaScript バイトコードインタプリタの jsinterp.c を参照して無名関数の生成から追ってみます。
  1. 5744           BEGIN_CASE(JSOP_ANONFUNOBJ)  
  2. 5745             /* Load the specified function object literal. */  
  3. 5746             LOAD_FUNCTION(0);  
  4. 5747   
  5. 5748             /* If re-parenting, push a clone of the function object. */  
  6. 5749             parent = js_GetScopeChain(cx, fp);  
  7. 5750             if (!parent)  
  8. 5751                 goto error;  
  9. 5752             obj = FUN_OBJECT(fun);  
  10. 5753             if (OBJ_GET_PARENT(cx, obj) != parent) {  
  11. 5754                 obj = js_CloneFunctionObject(cx, fun, parent);  
  12. 5755                 if (!obj)  
  13. 5756                     goto error;  
  14. 5757             }  
  15. 5758             PUSH_OPND(OBJECT_TO_JSVAL(obj));  
  16. 5759           END_CASE(JSOP_ANONFUNOBJ)  
ここでも LOAD_FUNCTION が使われているので定義を追ってみます。
jsinterp.c
  1. 2604 #define LOAD_FUNCTION(PCOFF)                                                  \  
  2. 2605     JS_GET_SCRIPT_FUNCTION(script, GET_FULL_INDEX(PCOFF), fun)  
JS_GET_SCRIPT_FUNCTION を実行するマクロになっています。

じゃあ JS_GET_SCRIPT_FUNCTION はどうなってるの
jsscript.h
  1. 143 #define JS_GET_SCRIPT_FUNCTION(script, index, fun)                            \  
  2. 144     JS_BEGIN_MACRO                                                            \  
  3. 145         JSObject *funobj_;                                                    \  
  4. 146                                                                               \  
  5. 147         JS_GET_SCRIPT_OBJECT(script, index, funobj_);                         \  
  6. 148         JS_ASSERT(HAS_FUNCTION_CLASS(funobj_));                               \  
  7. 149         JS_ASSERT(funobj_ == (JSObject *) STOBJ_GET_PRIVATE(funobj_));        \  
  8. 150         (fun) = (JSFunction *) funobj_;                                       \  
  9. 151         JS_ASSERT(FUN_INTERPRETED(fun));                                      \  
  10. 152     JS_END_MACRO  
JSFunction にキャストして JavaScript 関数オブジェクトになることが確認できました。
(これは現行の JavaScript だともっとてっとり早くなってる)

こんな感じで追ってみると、anonfunobj は無名関数の生成ということがわかります。

次の group は、
jsopcode.tbl で同じく定義されています。
  1. 317 /* Parenthesization opcode to help the decompiler. */  
  2. 318 OPDEF(JSOP_GROUP,       131, "group",       NULL,     1,  0,  0, 19,  JOF_BYTE)  
jsopcode.c では以下のあたり。
  1. 2066               case JSOP_GROUP:  
  2. 2067                 cs = &js_CodeSpec[lastop];  
  3. 2068                 if ((cs->prec != 0 &&  
  4. 2069                      cs->prec <= js_CodeSpec[NEXT_OP(pc)].prec) ||  
  5. 2070                     pc[JSOP_GROUP_LENGTH] == JSOP_NULL ||  
  6. 2071                     pc[JSOP_GROUP_LENGTH] == JSOP_DUP ||  
  7. 2072                     pc[JSOP_GROUP_LENGTH] == JSOP_IFEQ ||  
  8. 2073                     pc[JSOP_GROUP_LENGTH] == JSOP_IFNE) {  
  9. //                       ...(中略)...  
  10. 2100                 }  

無名関数オブジェクトを生成し、それを括弧 () によって式とし、
そしてそれを call というような流れになっています。


続いて (function(){}()) のほうも同じように。
$ js
js> dis(function() { (function(){}()) });
flags: LAMBDA INTERPRETED
main:
00000: anonfunobj (function () {})
00003: pushobj
00004: call 0
00007: pop
00008: stop

Source notes:
0: 4 [ 4] pcbase offset 4

なんと、(function(){})() と比べると
group がないだけで、ほかは同じになってしまいました。

無名関数に対する Parenthesization が発生しないので、
ちょうど、上で書いた [function(){}()] のように実行されています。

この結果から、

(function(){})() よりも、 (function(){}()) のほうが
コストが減るとわかりました。


とは言っても、どちらを使用しても実行時間が変化するとしてもマイクロ秒単位などで
JavaScript でそこまでクリティカルな処理が存在するのかも微妙なところで
あまり気にしないでいい気がします。


でも一応ベンチマークをとってみます。
  1. js> for (var t = +new Date, i = 0, f; i < 10000000; i++) f = (function(){}());  
  2.     print(+new Date - t);  
  3. 10437  
  4. js> for (var t = +new Date, i = 0, f; i < 10000000; i++) f = (function(){})();  
  5.     print(+new Date - t);  
  6. 10485  

結果:
  • (function(){}()) : 10437 ms.
  • (function(){})() : 10485 ms.

たしかに (function(){}()) のほうが速かったんですが、
10,000,000 回ループして わずか 48 ミリ秒の違い。
これを懸念してる人って…(いるのでしょうか)


そういえばこの違いについてはちょっと前に Twitter で話題(?) になって気がします。
もしかしてもうまとめられてたのかな…。だとしたら……考えたくない。


とりあえずモヤモヤした感じがなくなったのでよかった。
たまにソースコード確認してみると新鮮デスネ!


0 件のコメント:

コメントを投稿