技術評論社より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オブジェクトではなく、配列を用いるものとする。
このスクリプトは配列に納めた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)とその型指定をしっかり使うことが大切になる。
配列エレメントをforループで加える
つぎは、配列にエレメントを加える場合だ。もちろん、変数と型指定をしっかりすべきことは、前項と変わらない。それに加えて、最適化を考えるときに知っておきたいことを紹介しよう。また、課題のスクリプトを先に掲げる。0から9までの連番整数をエレメントとする配列が、forループの処理でできあがる。
これは難問だろう。ActionScript 3.0の教科書に出てきてもおかしくはないスクリプトだ。もし、筆者がこの処理に問題があるかと問われたら、ないと答える。だが、少しでも速いタイムをたたき出したいとするなら、ふたつ手が加えられる。答えも先に出してしまうことにする。
- 配列エレメントを加えるforループの最適化】
- 配列インスタンスは配列アクセス演算子[]でつくる
- 配列エレメントは配列アクセス演算子[]で加える
これを見て、配列アクセス演算子[]は処理が速いのかと思うかもしれない。結果だけならそう覚えてもよい。けれど、理解としては正しくない。配列アクセス演算子[]の処理そのものは、別に速くないからだ。配列アクセス演算子[]を使うとなぜ速いのか知るには、Flash Playerの立場で考えなければならない。ほとんどの読者はFlash Playerの立場など想像したこともなかろう。これを機会にちょっと考えてみてほしい。
コンストラクタよりもリテラル記述が速いのは
メソッドからインスタンスを得ることなく、プログラムに直接記述される値は「リテラル」という。new演算子でArray()コンストラクタメソッドを呼出すより、配列アクセス演算子[]で配列をリテラルで書く方が速いのは、Arrayクラスのコンストラクタがオーバーロードされているからだ。
「オーバーロード」(多重定義)というのは、同じ名前のメソッドが複数定められていることを意味する。[ヘルプ]で[Array]クラスのメソッドを調べると、実際コンストラクタメソッドがふたつある(図1)。このふたつをどうやって使い分けるかというと、コンストラクタに渡される引数が鍵になる。
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)。
それに対して、配列アクセス演算子を用いてリテラルで書くとき、演算子[]の中に加えられるのはエレメントだけだ。値が何を意味するのか確かめる必要がない。このひと手間の差が、処理の速さにつながるのだ。
Array.push()メソッドよりも配列アクセスが速いのは
つぎは、Array.push()メソッドよりも、配列アクセス演算子[]でエレメントを加える方がなぜ速いのかだ。Array.push()メソッドは、[ヘルプ]につぎのように説明されている。
エレメントを配列の最後に追加して、追加後の配列の長さを返します。
ここでまた、Flash Playerの立場で仕事を考える。すると、Array.push()メソッドで配列エレメントを加えるには、配列の最後尾を探さなければならないことに気づく(図3)。具体的には、Array.lengthプロパティの値を調べることになる。それに対して、配列アクセスを使うときには、エレメントを加えるべきインデックスが演算子[]にはっきりと示されていなければならない。つまり、最後尾を探す手間がないのだ。
このようにFlash Playerの立場で考えることは、ただおまじないのように最適化テクニックを覚えるのと違い、ActionScript 3.0の処理をより深く知ることにつながる。結果に至る筋道がわかれば、別の処理についても推論できる。
たとえば、予め長さが決められない配列にエレメントを加えたいとする。エレメントは配列アクセス演算子[]で加える方が速いといった。すると、配列が変数my_arrayに納められている場合、このように書くことになろう。
しかし、このステートメントの処理は、Array.push()メソッドを使った場合と比べて速さはほぼ変わらない。勘のよい読者は、Array.lengthプロパティで最後尾を調べているからだということに気づかれたと思う。「配列アクセス演算子[]の処理そのものは、別に速くない」と前に述べたのは、つまりこのような意味である。
エレメントのインデックスはuint型で渡す
結びは、配列アクセス演算子[]に渡すインデックスについての注意だ。インデックスは必ずuint型にしよう。そんなことはわかりきっていると思われるかもしれない。しかし、こういう場合はどうだろう。ある配列のエレメントを、インデックスが偶数と奇数のふたつの配列に分ける。たとえば、つぎのような配列(my_array)だ。
もとの配列(my_array)の長さ(Array.lengthプロパティ値)が必ず偶数なら、つぎのようなスクリプトが考えられる。動作に問題はなく、エレメントは奇数と偶数のインデックスに分けられて、ふたつの配列(even_arrayとodd_array)ができあがる(図4)。
forループで用いるカウンタ変数(i)はuint型で定めた。しかし、偶数と奇数の配列インデックスを求めるため、その変数に乗算と加算を行っている。そうすると、計算結果には型指定が及ばなくなる。したがって、最適化するためには、式全体をuint()関数の引数に渡し、整数に変換した方がよい[1]。
この特別編の次回(第58回)は、スクリプトのアニメーションとオブジェクトの使い回しテクニックを紹介するつもりだ。