ActionScript3 ブロックスコープの ABC

※ AS3 にはブロックスコープがないよ、という内容です

一時変数の効率化|_level0.KAYAC という記事に怪しいことが書いてあったので突っ込んでおきます。

この記事によると、for の中で変数を宣言するよりも

// 中バージョン
function foo1():void{
  for(var i:int = 0; i < 10; i++){
    var a:Object = new Object();
  }
}

外で宣言をしたほうが

// 外バージョン
function foo2():void{
  var a:Object;
  for(var i:int = 0; i < 10; i++){
    a = new Object();
  }
}

a が宣言される回数が少ないので効率的だとしています。

けれども、これは間違いです。

for の中で宣言したとしても、for のあとで変数は生き残ってます。さらに、for の前でも参照できます。

実証コードを見てみよう

以下に実証コードを。

trace(a);                       // NaN

for(var i = 0; i < 10; i++){
  var a:Number = new Number(i);
  trace(a);                     // 0, 1, ..., 9
}

trace(a);                       // 9

for の中で var a を宣言していますが、for を抜けたあとも 9 ですね。また、for の前では NaN(Number の初期値) になっています。

ということで、var は実はどこに書いても同じ。全て関数の冒頭に宣言した場合と同じになるわけです。

ブロックスコープと let

この性質のことを専門用語でいうと、「ActionScript 3はブロックスコープを持たない」といえます。var 宣言は 関数スコープ 内に変数を作成します。

そういえば、次の警告に出くわしたことある人も多いのでは。

var a;
for(var i = 0; i < 10; i++){
  // 警告: 変数定義が重複しています。
  var a:Number = new Number(i);
}

この警告からも、AS3 がブロックスコープを持たないのが分かりますね。

対して、C++ や Java はブロックスコープを持つので、ブロックでだけ有効な変数を宣言することができます。

ちなみに、JavaScript だと with を使ってブロックスコープを再現できるのですが、AS3 では with はなかったことになってるので使えません。(関連)for 文と無名関数のイディオム - IT戦記

また、JavaScript 1.7 からは let を使ってブロックスコープを宣言することができます。

ECMAScript 3.1 にも取り込まれるはずです (追記)←取り込まれない方向のようです。失礼しました。

バイトコードを見てみた

ここからが本題。

せっかくなので

  • var を for の中に置く場合 (中バージョン)
  • var を for の外に置く場合 (外バージョン)

でバイトコードがどう変わるかを見てみた。

これが検証用のソースコード。

// 中バージョン
function foo1():void{
  for(var i:int = 0; i < 10; i++){
    var a:Object = new Object();
  }
}

// 外バージョン
function foo2():void{
  var a:Object;
  for(var i:int = 0; i < 10; i++){
    a = new Object();
  }
}

Flex SDK 3.2 の asc.jar でコンパイルして、Tamarin 付属の abcdump でバイトコードを眺めてみた。

中バージョンではこんな感じになった。

var undefined():void    /* disp_id 0*/
{
  // local_count=3 max_scope=0 max_stack=2 code_len=33
  0         pushnull
  1         coerce              Object
  3         setlocal2
  4         pushbyte            0
  6         convert_i
  7         setlocal1
  8         jump                L1

  L2:
  12        label
  13        findpropstrict      Object
  15        constructprop       Object (0)
  18        coerce              Object
  20        setlocal2
  21        getlocal1
  22        increment_i
  23        convert_i
  24        setlocal1

  L1:
  25        getlocal1
  26        pushbyte            10
  28        iflt                L2

  32        returnvoid
}

いっぱい出てきたが、焦らずにゆっくり見ていこう。

最初にローカル変数の準備

まずは冒頭の7行。

コメントでバイトコードにほぼ等しい AS3 の該当するコードを補っておいた。

// var a:Object = null
  0  pushnull          // null をスタックに積む
  1  coerce     Object // スタック1番上を Object にキャスト
  3  setlocal2         // pop して register 2 に代入

// var i:int = 0
  4  pushbyte   0      // 0 をスタックに積む
  6  convert_i         // スタック1番上を int にキャストする
  7  setlocal1         // pop して register 1 に代入

  8  jump       L1     // L1 に移動

関数の最初ではローカル変数 a と i を準備していることが分かる。

ここまでのバイトコードは、「外バージョン」も「中バージョン」も同じだった。ローカル変数は宣言した場所によらず、関数の冒頭で確保されるようだ。

for を抜けるチェック

次、L1 の処理を見ていく。

L1:
// i < 10
  25 getlocal1         // i (register 1)をスタックに積む
  26 pushbyte   10     // 10 をスタックに積む
  28 iflt       L2     // スタック上の2つの値を比較して、
                       // i が小さければ L2 にジャンプする

  32 returnvoid        // void を返す

i < 10 である限りは、L2 にジャンプし続けるわけですな。実はここも、外バージョンと中バージョンで同じ。

いよいよ for の中を解析

最後に L2。ここで外と中の違いがでてくる。

まずは中バージョン(var a:Object = new Object())。

L2:
  12  label

// a = new Object()
  13 findpropstrict  Object
  15 constructprop   Object (0) // new Object() する
  18 coerce          Object     // Object にキャストする
  20 setlocal2                  // a (register 2) に代入

// i++
  21 getlocal1        // i (register 1) をスタックに取り出す
  22 increment_i      // スタック1番上を 1 加算する
  23 convert_i        // スタック1番上を int にキャストする
  24 setlocal1        // スタック1番上を register 1 に代入

素直な感じ。

次は外バージョン(a = new Object())。

// var c:* = new Object()
  13 findpropstrict  Object
  15 constructprop   Object (0) // new Object() する
  18 dup                        // スタック上で複製する
  19 setlocal3                  // pop して register 3 に代入

// a = (new の結果)
  20 coerce    Object // スタック1番上を Object にキャストする
  22 setlocal1        // a (register 2) に代入

// c = null
  23 getlocal3        // c (register 3) をスタックに複製する
  24 kill      3      // register 3 を undefined に初期化する
  26 pop              // スタックの1番上を取り除く

// i++
  27 getlocal2        // i (register 1) をスタックに取り出す
  28 increment_i      // スタック1番上を 1 加算する
  29 convert_i        // スタック1番上を int にキャストする
  30 setlocal2        // スタック1番上を register 1 に代入

ということで、new の結果を一時変数 var c:* に代入したような扱いになっている。

一時変数がどこからでてくるのかがよく分からなかったが、もしかしたら、

x = y = new Object();

のような構文で y = new Object() の結果を x に代入するための一時オブジェクトなのかもしれない。

C++ でいうコピーコンストラクタと代入演算子の違いのような感じ。

バイトコードを見た感想

こうやって見ると、

// 中バージョン
function foo1():void{
  for(var i:int = 0; i < 10; i++){
    var a:Object = new Object();
  }
}

の方が

// 外バージョン
function foo2():void{
  var a:Object;
  for(var i:int = 0; i < 10; i++){
    a = new Object();
  }
}

よりも多少効率がよかった。とはいえ、ほんとうに些細な違いであり、無視していいレベルのはずだ。普通にコードを書いたら、ボトルネックは別の場所に出てくるだろう。

結論

  • AS3 は関数スコープ
  • 書きやすいほうで書け