ActionScript 3.0で始めるオブジェクト指向スクリプティング

第56回特別編】配列エレメントすべてをforループで扱う

技術評論社より2月10日に発売された拙著ActionScript 3.0パフォーマンスチューニングから、ActionScript 3.0の最適化の仕方をご紹介する特別編の2回目は、配列エレメントすべてをforループでいかに速く扱うかだ。でも、この特別編の1回目第54回に配列よりVectorクラスを使えといったではないか、と訝る向きもあろう。そのとおりだ、Vectorオブジェクトが使えるなら、使ったほうがよい。

だが、Vectorクラスを使うためには条件があった(第54回「⁠⁠特別編】配列の処理をVectorオブジェクトで最適化する」Vectorクラスのおさらい参照⁠⁠。Flash Playerのバージョンも10以降でなければならない。そのため、配列を用いるしかない場合がある。それに、Vectorクラスで使えるテクニックは、文法の違いを除いて、ほぼ配列でも通用する。しかし逆に、配列にはVectorクラスとは違ったコツがある。したがって、今回は配列をお題とする。

配列エレメントをforループで取出す

まずは、配列エレメントをforループで取出して操作する場合だ。もっとも、配列の扱いやforループで気をつけるべきことは、これまでの連載で説明してきた。そこで、つぎのスクリプトを見て、最適化するにはどこを直したらよいか考えてほしい。配列(my_array)に納めたエレメント(my0_mc~my2_mc)はMovieClipインスタンスだ。ただし、前述のとおりVectorオブジェクトではなく、配列を用いるものとする。

var my_array:Array = [my0_mc, my1_mc, my2_mc];
for (var i:Number = 0; i < my_array.length; i++) {
  my_array[i].x = 10;
  my_array[i].y = 10;
}

このスクリプトは配列に納めたMovieClipインスタンスをforループですべて取出して、xy座標を定めている。文法的に何の問題もなく、すべてのインスタンスの位置が正しく決まる。しかし、処理を速める余地が3つある。

第1は、forループで用いるカウンタ変数(i)の型指定だ。Number型は小数値を含む(⁠⁠浮動小数点数」という⁠⁠。だが、カウンタには整数を使う。しかも、配列インデックスとしても用いるなら、マイナスにはならない。そうした場合には、同じ数値でもuint型で宣言する方がよい。

第2に、forステートメントの継続条件に用いられているArray.lengthプロパティの参照だ。継続条件は、繰返す処理がひとつ終わるごとに確かめられる。つまり、ループ回数分参照されることになる。こういう値は予め変数に変数に入れて、それを参照する方が速い。とくに、関数の中でローカル変数にとると、さらにアクセスは速まる。

第3は、配列のMovieClipインスタンスを変数に取出していないことだ。一般に、配列アクセス演算子[]で同じエレメントを幾度も参照するより、変数に入れてしまった方がアクセスは速い。これは前述第2と同じ考慮といえる。加えて、配列エレメントには、Vectorオブジェクトと違って型指定できないことを思い出してほしい。ActionScript 3.0は、型指定されたデータはメモリや処理を最適化して扱う。配列エレメントを型指定した変数に納めなければ、この最適化の恩恵が受けられないのである。

配列エレメントを取出すforループの最適化
  • 配列インデックスとカウンタ変数は整数型(int/uint)で指定
  • 継続条件に用いるArray.lengthプロパティは予め変数にとる
  • 取出したエレメントは型指定した変数に入れる

これら3つの修正を加えて書直したのが、つぎのフレームアクションだ。変数varとその型指定をしっかり使うことが大切になる。

var my_array:Array = [my0_mc, my1_mc, my2_mc];
var nLenght:uint = my_array.length;
for (var i:uint = 0; i var my_mc:MovieClip = my_array[i];
  my_mc.x = 10;
  my_my.y = 10;
}

配列エレメントをforループで加える

つぎは、配列にエレメントを加える場合だ。もちろん、変数と型指定をしっかりすべきことは、前項と変わらない。それに加えて、最適化を考えるときに知っておきたいことを紹介しよう。また、課題のスクリプトを先に掲げる。0から9までの連番整数をエレメントとする配列が、forループの処理でできあがる。

var my_array:Array = new Array();
for (var i:uint = 0; i < 10; i++) {
  my_array.push(i);
}

これは難問だろう。ActionScript 3.0の教科書に出てきてもおかしくはないスクリプトだ。もし、筆者がこの処理に問題があるかと問われたら、ないと答える。だが、少しでも速いタイムをたたき出したいとするなら、ふたつ手が加えられる。答えも先に出してしまうことにする。

配列エレメントを加えるforループの最適化】
  • 配列インスタンスは配列アクセス演算子[]でつくる
  • 配列エレメントは配列アクセス演算子[]で加える
var my_array:Array = [];
for (var i:uint = 0; i < 10; i++) {
  my_array[i] = i;
}

これを見て、配列アクセス演算子[]は処理が速いのかと思うかもしれない。結果だけならそう覚えてもよい。けれど、理解としては正しくない。配列アクセス演算子[]の処理そのものは、別に速くないからだ。配列アクセス演算子[]を使うとなぜ速いのか知るには、Flash Playerの立場で考えなければならない。ほとんどの読者はFlash Playerの立場など想像したこともなかろう。これを機会にちょっと考えてみてほしい。

コンストラクタよりもリテラル記述が速いのは

メソッドからインスタンスを得ることなく、プログラムに直接記述される値は「リテラル」という。new演算子でArray()コンストラクタメソッドを呼出すより、配列アクセス演算子[]で配列をリテラルで書く方が速いのは、Arrayクラスのコンストラクタがオーバーロードされているからだ。

「オーバーロード」⁠多重定義)というのは、同じ名前のメソッドが複数定められていることを意味する。[ヘルプ]で[Array]クラスのメソッドを調べると、実際コンストラクタメソッドがふたつある図1⁠。このふたつをどうやって使い分けるかというと、コンストラクタに渡される引数が鍵になる。

図1 Arrayクラスのふたつのコンストラクタメソッド
図1 Arrayクラスのふたつのコンストラクタメソッド

Array()コンストラクタメソッドには、引数がいくつでも渡せる。そして、多くの場合それらの引数は、新たにつくられる配列のエレメントとして納められる。しかし、整数をひとつだけ渡すと、もうひとつのコンストラクタが呼出されて、引数値の長さ(エレメント数)の配列がつくられる表1⁠。

表1 コンストラクタメソッドの引数によってつくられる配列の違い
コンストラクタ呼出し引数の意味つくられる配列
new Array()エレメント[]
new Array(3)長さ[undefined, undefined, undefined]
new Array(0, 1, 2)エレメント[0, 1, 2]
new Array("a")エレメント["a"]

さあ、ここでFlash Playerの立場で考えてみよう。コンストラクタが呼出された場合、まず引数の数を確かめなければならない。複数ならそれらはエレメントだ。しかし、引数がひとつだったら、さらにそれが整数かどうかを見る。整数なら引数は長さ(エレメント数)の指定、そうでなければエレメントがひとつ渡されたことになる。このように確かめていって初めて引数の意味が定まり、どちらのコンストラクタを呼出せばよいのかも決まる図2⁠。

図2 引数の数とデータ型でその意味と呼出すコンストラクタが決まる
図2 引数の数とデータ型でその意味と呼出すコンストラクタが決まる

それに対して、配列アクセス演算子を用いてリテラルで書くとき、演算子[]の中に加えられるのはエレメントだけだ。値が何を意味するのか確かめる必要がない。このひと手間の差が、処理の速さにつながるのだ。

Array.push()メソッドよりも配列アクセスが速いのは

つぎは、Array.push()メソッドよりも、配列アクセス演算子[]でエレメントを加える方がなぜ速いのかだ。Array.push()メソッドは、[ヘルプ]につぎのように説明されている。

エレメントを配列の最後に追加して、追加後の配列の長さを返します。

ここでまた、Flash Playerの立場で仕事を考える。すると、Array.push()メソッドで配列エレメントを加えるには、配列の最後尾を探さなければならないことに気づく図3⁠。具体的には、Array.lengthプロパティの値を調べることになる。それに対して、配列アクセスを使うときには、エレメントを加えるべきインデックスが演算子[]にはっきりと示されていなければならない。つまり、最後尾を探す手間がないのだ。

図3 配列の最後尾を探さないとけいない
図3 配列の最後尾を探さないとけいない

このようにFlash Playerの立場で考えることは、ただおまじないのように最適化テクニックを覚えるのと違い、ActionScript 3.0の処理をより深く知ることにつながる。結果に至る筋道がわかれば、別の処理についても推論できる。

たとえば、予め長さが決められない配列にエレメントを加えたいとする。エレメントは配列アクセス演算子[]で加える方が速いといった。すると、配列が変数my_arrayに納められている場合、このように書くことになろう。

my_array[my_array.length] = エレメント;

しかし、このステートメントの処理は、Array.push()メソッドを使った場合と比べて速さはほぼ変わらない。勘のよい読者は、Array.lengthプロパティで最後尾を調べているからだということに気づかれたと思う。⁠配列アクセス演算子[]の処理そのものは、別に速くない」と前に述べたのは、つまりこのような意味である。

エレメントのインデックスはuint型で渡す

結びは、配列アクセス演算子[]に渡すインデックスについての注意だ。インデックスは必ずuint型にしよう。そんなことはわかりきっていると思われるかもしれない。しかし、こういう場合はどうだろう。ある配列のエレメントを、インデックスが偶数と奇数のふたつの配列に分ける。たとえば、つぎのような配列(my_array)だ。

var my_array:Array = [0, 1, 2, 3, 4, 5];
    
・インデックスが偶数のエレメントの配列: [0, 2, 4]
・インデックスが奇数のエレメントの配列: [1, 3, 5]

もとの配列(my_array)の長さArray.lengthプロパティ値)が必ず偶数なら、つぎのようなスクリプトが考えられる。動作に問題はなく、エレメントは奇数と偶数のインデックスに分けられて、ふたつの配列(even_arrayとodd_array)ができあがる図4⁠。

var my_array:Array = [0, 1, 2, 3, 4, 5];
var nLength:uint = my_array.length / 2;
var even_array:Array = [];
var odd_array:Array = [];
for (var i:uint = 0; i < nLength; i++) {
  even_array[i] = my_array[i * 2];
  odd_array[i] = my_array[i * 2 + 1];
}
図4 ひとつの配列をインデックスが偶数と奇数のふたつの配列に分ける
図4 ひとつの配列をインデックスが偶数と奇数のふたつの配列に分ける 図4 ひとつの配列をインデックスが偶数と奇数のふたつの配列に分ける

forループで用いるカウンタ変数(i)はuint型で定めた。しかし、偶数と奇数の配列インデックスを求めるため、その変数に乗算と加算を行っている。そうすると、計算結果には型指定が及ばなくなる。したがって、最適化するためには、式全体をuint()関数の引数に渡し、整数に変換した方がよい[1]⁠。

var my_array:Array = [0, 1, 2, 3, 4, 5];
var nLength:uint = my_array.length / 2;
var even_array:Array = [];
var odd_array:Array = [];
for (var i:uint = 0; i < nLength; i++) {
  even_array[i] = my_array[uint(i * 2)];
  odd_array[i] = my_array[uint(i * 2 + 1)];
}

この特別編の次回(第58回)は、スクリプトのアニメーションとオブジェクトの使い回しテクニックを紹介するつもりだ。

var n:uint = i * 2;
even_array[i] = my_array[n];

おすすめ記事

記事・ニュース一覧