サイトトップ

Director Flash 書籍 業務内容 プロフィール
 

Adobe Flash ActionScript講座
ActionScript 3.0による
三次元表現ガイドブック
ActionScript 3.0
プロフェッショナルガイド
■Twitter: @FumioNonaka / Mailing List: ActionScript 3.0

F-siteセミナー「みんなのFlash効率化大作戦」

ActionScriptでの最速を求める

Date: 2012年2月4日 Product: Flash Professional Platform: All Version: CS5/ActionScript 3.0

ActionScript 3.0 Performance Tuning
ActionScript 3.0
パフォーマンス
チューニング

Now on Sale!!

To Be Continued...
JaGraプロフェッショナルDTP&Webスクール無料セミナー(2012年2月25日土曜日開催)
ActionScript 3.0 パフォーマンスチューニング
− 速い、軽い、うまいスクリプティングを目指す[応用編]
」(レジュメおよびサンプル)

2012年2月4日土曜日のF-siteセミナーdemo4で話した内容に、時間の都合で割愛した解説も加えた。


01 型から入ろう

01-01 データ型を定める

変数(プロパティ)や関数(メソッド)には必ず型指定する。スクリプトは見やすく、扱いが速くなり、間違えればエラーを示してくれる。

【変数の型指定】
var 変数:データ型;
変数 = 値;

var 変数:データ型 = 値;

【関数の型指定】
function 関数(引数:データ型):戻り値のデータ型 {
  // 処理内容
  return 戻り値;
}

たとえば、回数をカウントアップ/ダウンするカウンタ変数は、繰返し処理(forループなど)でよく使われる。

【forステートメントによる繰返し処理】
for (初期値設定; 継続条件; 更新処理) {
  // 繰返す処理
}

もちろん、変数に型指定しないのは論外。

【型指定がないのは問題外】

for (var i = 0; i < 10; i++) {
  // 繰返す処理
}

回数は整数なので、小数値を含むNumberでも不十分。

【整数にNumber型の指定は不十分】

for (var i:Number = 0; i < 10; i++) {
  // 繰返す処理
}

整数型のintまたはuintで定めることにより最適化される。

【整数にuint型指定で最適化】

for (var i:uint = 0; i < 10; i++) {
  // 繰返す処理
}

01-02 Vectorクラスを使う

Vectorクラスのインスタンスはエレメントのデータ型が決まった配列。可能であれば配列よりVectorインスタンスを使おう。

    【Vectorクラスのよいところ】
  • エレメントにデータ型が定まる。
  • エレメントの扱いが速い。

Vectorクラスには配列にないおきてが決まっている。

    【Vectorクラスのおきて】
  • エレメントにただひとつのデータ型を定める。
  • エレメントのインデックスは0から始まる連番。

インスタンスはコンストラクタメソッドでつくるのがActionScript 3.0のお約束。ただし、エレメントのデータ型(ベース型)は、ちょっと妙な書き方をする。

【Vectorインスタンスをつくる】
new Vector.<データ型>()

Vectorインスタンスの生成とエレメントの値の追加、取出しは、つぎのように行う(スクリプト001)。ベース型は整数uintとした。

スクリプト001■Vectorインスタンスの生成とエレメントの追加・取出し

var myVector:Vector.<uint> = new Vector.<uint>();
myVector.push(100, 200, 300);
trace(myVector);   // 出力: 100,200,300
trace(myVector.length);   // 出力: 3
var index02:uint = myVector[2]; trace(index02);   // 出力: 300

それでは、配列からVectorオブジェクトに書替える例を示す。お題は、キーボードの矢印キーで操作したいとき(図001)。

図001■上下左右の矢印キーでインスタンスを動かす

[1]条件による判別

キーコード(KeyboardEvent.keyCodeプロパティ)の値により条件分岐するのがオーソドックスだ(「ActionScript 3.0で始めるオブジェクト指向スクリプティング」第13回「キーボードによる操作」参照)[*1]

【if条件でキーコードを判別】

stage.addEventListener(KeyboardEvent.KEY_DOWN, xKeyDown);
function xKeyDown(eventObject:KeyboardEvent):void {
  var nKeyCode:int = eventObject.keyCode;
  if (nKeyCode == Keyboard.LEFT) {
    // 左矢印キーの処理
  } else if (nKeyCode == Keyboard.RIGHT) {
    // 右矢印キーの処理
  } else if (nKeyCode == Keyboard.UP) {
    // 上矢印キーの処理
  } else if (nKeyCode == Keyboard.DOWN) {
    // 下矢印キーの処理
  }
}

[2]配列からデータを得る

キーコードは正の整数で重複がない(一意の)ため、配列との相性がよい。

表001■Keyboardクラスの矢印キーの定数
定数 キー
Keyboard.DOWN 下矢印キー 40
Keyboard.LEFT 左矢印キー 37
Keyboard.RIGHT 右矢印キー 39
Keyboard.UP 上矢印キー 38

配列の操作したい矢印キーのコードのインデックス(37〜40)に、そのキーの処理に必要なデータを入れる。

【配列のキーコードのインデックスに処理用のデータを入れる】

var keys_array:Array = new Array();
keys_array[Keyboard.LEFT] = 左矢印キー用のデータ;
keys_array[Keyboard.RIGHT] = 右矢印キー用のデータ;
keys_array[Keyboard.UP] = 上矢印キー用のデータ;
keys_array[Keyboard.DOWN] = 下矢印キー用のデータ;
stage.addEventListener(KeyboardEvent.KEY_DOWN, xKeyDown);
function xKeyDown(eventObject:KeyboardEvent):void {
  var nKeyCode:int = eventObject.keyCode;
  var key_array:Array = keys_array[nKeyCode];   // キーコードのエレメント取出し
  if (key_array) {   // エレメントが存在すれば
    // その矢印キーの処理
  }
}

リスナー関数で押されたキーのコードを調べたら、配列からそのインデックスのエレメントを取出す。矢印キーのキーコード(37〜40)以外にはエレメントがない。if条件でエレメントの存在を確かめたうえで、その矢印キーの処理を行う(「ActionScript 3.0で始めるオブジェクト指向スクリプティング」第15回
「配列を使ったキーコードとプロパティの扱い」の「配列アクセス演算子でプロパティを操作する」の項参照)。

[3]長さを定めたVectorオブジェクトからデータ取得

この仕組みをVectorオブジェクトに移そうとしたとき、問題はエレメントが0からの連番で納められていない(0〜36のエレメントがない)こと。解決するには、連番のエレメントを(0〜36に)埋めてしまえばよい。Vector()コンストラクタメソッドの第1引数には、長さ(エレメント数)が定められる(第2引数はその固定)。

【長さが与えられたVectorインスタンスをつくる】
new Vector.<データ型>(長さ, 長さの固定)

Vectorインスタンスに予め長さを定めておくと、エレメントの扱いが速くなる[*2]

    【Vectorオブジェクトの最適化】
  • 予め長さを定める(正の整数)
  • できれば長さを固定する(true)

キーボードのキーコードは今のところ200ほどなので(ヘルプ[Adobe Flash ActionScript 2.0の学習]/[キーボードのキーとキーコードの値]参照)、長さは300にした。また、長さの固定にはtrueを渡している[*3]

【Vectorオブジェクトのキーコードのインデックスに処理用のデータを入れる】

var keys:Vector.<ベース型> = new Vector.<ベース型>(300, true);
keys[Keyboard.LEFT] = 左矢印キー用のデータ;
keys[Keyboard.RIGHT] = 右矢印キー用のデータ;
keys[Keyboard.UP] = 上矢印キー用のデータ;
keys[Keyboard.DOWN] = 下矢印キー用のデータ;
stage.addEventListener(KeyboardEvent.KEY_DOWN, xKeyDown);
function xKeyDown(eventObject:KeyboardEvent):void {
  var nKeyCode:int = eventObject.keyCode;
  var key:Array = keys[nKeyCode];   // キーコードのエレメント取出し
  if (key) {   // エレメントが存在すれば
    // その矢印キーの処理
  }
}

このお題について詳しくは、「ActionScript 3.0で始めるオブジェクト指向スクリプティング」の第54回「【特別編】配列の処理をVectorオブジェクトで最適化する」に解説した。

01-03 forループで配列・Vectorを扱うときのお約束

配列のエレメントすべてに操作を加えるとき、forステートメントが用いられる。まず、エレメントをすべて取出すとき。

【配列エレメントをすべて取出す最適化されていない例】

for (var i:Number = 0; i < my_array.length; i++) {
  trace(my_array[i]);
}

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

var nLength:uint = my_array.length;
for (var i:uint = 0; i < nLength; i++) {
  var element:データ型 = my_array[i];
  trace(element);
}

(1)カウンタ変数と配列インデックスは整数型で定める。(2)繰返し判定される継続条件では、プロパティに毎回アクセスするのでなく、値を変数にとっておく。(3)配列エレメントは型が決まっていないので、型指定された変数に入れる。

つぎに、新たにつくった配列に、エレメントを加える場合。お題は、連番整数を配列エレメントに納める。

【新規配列にエレメントを加える最適化されていない例】

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

    【配列エレメントを加えるforループの最適化】
  • 配列は配列アクセス演算子[]でつくる
  • 配列エレメントは配列アクセス演算子[]で加える
【新規配列にエレメントを加える最適化された例】

var my_array:Array = [];
for (var i:uint = 0; i < 10; i++) {
  my_array[i] = i;
}

配列を操作するforループの最適化の考え方は、Vectorオブジェクトにも基本的に当てはまる[*4]

01-04 Flash Playerの仕事を考える

前項では、配列の生成や追加は配列アクセス演算子[](bracket)が速いことを知った。その理由はFlash Playerの仕事を考えれば見当がつく。

[1]配列の生成

Array()コンストラクタメソッドは実はふたつある(オーバーロード)。

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

渡した引数の数とそのデータ型によって呼出されるコンストラクタメソッドが変わり、つくられる配列も異なる(表001)。

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

配列アクセス演算子[]でつくるのは、指定されたエレメントをもつ配列と決まっている。引数の判別がない分、呼出しは速まる。

[2]配列エレメントの追加

Array.push()メソッドは、配列の最後にエレメントを加えるので、その長さ(Array.lengthプロパティの値)を調べなければならない。配列アクセス[]なら、長さは確かめなくて済む。とくに、関数内でforループを使うときは、インデックスに用いられるカウンタはローカル変数なので、さらに速さが稼げる[*5]

長さを調べると、Array.push()メソッドと速さは変わらない。

my_array[my_array.length] = 値;

[*1] 条件式の等価比較は左辺が同じなので、switch-caseステートメントを使ってもよい(「ActionScript 3.0で始めるオブジェクト指向スクリプティング」第14回「キー操作とif以外の条件判定」の「switchステートメント」の項参照)。

[*2] Vectorオブジェクトへのエレメントの追加とメモリ領域の割当てについて、[ヘルプ]にはつぎのように説明されている([モバイル]/[Flash Platformのパフォーマンスの最適化]/[ActionScript 3.0のパフォーマンス]/[VectorクラスとArrayクラス]の日本語訳はわかりにくいので、英語原文より筆者が邦訳)。

Vectorの大きさが前もって定められていないと、空きが足りなくなったときに増やされます。Vectorの大きさが増えるたびに、新たなメモリ領域が割当てられます。そのときのVectorの中身は、その新たなメモリ領域にコピーされます。追加の割当てとデータのコピーは、パフォーマンスを損なうことになります。

なお、Rest Term「Flashにおけるメモリ管理」の検証が参考になる。

[*3] さらにエレメントのデータにもとづく矢印キーの処理を工夫することで、条件判定による処理と速さの違いはほぼなくなる(F-site「キーボードのキー操作をVectorオブジェクトで扱う」参照)。

[*4] 新たなVectorオブジェクトをつくるときは、new演算子でコンストラクタメソッドを呼出し、引数に長さを渡す。

[*5] 関数の外でvar宣言した変数(フレームアクションの「タイムライン変数」やクラス定義の「プロパティ」)より、関数内で宣言する「ローカル変数」の方がアクセスは速い。「ローカル変数」の基礎知識については、拙著『ActionScript 3.0プロフェッショナルガイド』Chapter 2「スクリプトによるアニメーション」(PDFプレビュー公開)2.6「タイムライン変数とローカル変数」p.075〜をお読みいただきたい。また、クラス定義した場合について、BeInteractive!「ループの最適化」参照。


02 1/0で数える

02-01 2進数を思い出そう

2進数は2で繰上がり、数字は1と0のみで表される。桁は増えやすいものの、計算の規則は1と0のふたつの数字しかないので単純。

10進数は、1桁繰上がるごとに10倍になる。たとえば、10進数123 = 1×100 + 2×10 + 3×1 = 1×102 + 2×101 + 3×100である。k桁目の数をnkとすると、一般につぎのように表される。

【10進数】
nk×10k-1 + ... + n2×101 + n1×100

2進数は、1桁繰上がるごとに2倍になる。たとえば、2進数1011(2) = 1×23 + 0×22 + 1×21 + 1×20 = 8 + 0 + 2 + 1 = 11である[*6]。なお、数字に添えた(2)は2進数を意味する。

【2進数】
nk×2k-1 + ... + n2×21 + n1×20

02-02 2進数が使われる例 − Array定数

Array.sort()メソッドにオプションの引数をArray定数(表001)で渡すと、ソートの仕方が変えられる。Array定数の値は整数で、2進数により定められている(表001)。

表002■Arrayクラスの定数と与えられた整数値
Arrayクラスの定数 ソートの指定 与えられた10進数 2進数表現
CASEINSENSITIVE 大文字小文字を区別しない 1 = 20 1
DESCENDING 値の大きい順(降順) 2 = 21 10
UNIQUESORT 重複のないソート 4 = 22 100
RETURNINDEXEDARRAY ソートしたインデックスの配列を返す 8 = 23 1000
NUMERIC 数値によるソート 16 = 24 10000

定数値は互いに異なる桁が1なので、複数のオプションは加算することにより一意に定まる。したがって、数値(Array.NUMERIC = 16)の大きい順(Array.DESCENDING = 2)にソートしたければ、ふたつの整数値の和である18(= 16 + 2)をメソッドの引数に渡す[*7]

var my_array:Array = [1000, 40, 300, 2];
my_array.sort();
trace(my_array);   // 出力: 1000,2,300,40
my_array.sort(18);
trace(my_array);   // 出力: 1000,300,40,2

02-03 ビット演算とは

[ヘルプ]では複数のArray定数をまとめるとき、加算演算子(+)ではなく「ビット単位の論理和(OR)(|)演算子」を使うとされる。もっとも、加算をした場合と演算の結果は変わらない(16 + 2 = 16 | 2 = 18)。

my_array.sort(Array.NUMERIC | Array.DESCENDING);
// もちろん以下でもよい
// my_array.sort(16 | 2);

ビット」は2進数のひと桁を指す。ビット単位の論理演算は、2進数を各桁つまりビットごとに計算し、他の桁は触らない。つまり、桁をまたがる繰り上がりや繰り下がりがない。その分計算ルールは単純で、早い処理が期待できる。

ビット単位の論理和演算は、条件のOR(論理和)||と同じ考え方で、演算対象(オペランド)のどちらか一方が1なら1、そうでなければ0になる(表003)。

表003■ビット単位の論理和演算子|の演算結果
ビット単位の論理和演算 結果の2進数
0 | 0 0
0 | 1
1 | 0
1
1 | 1 1

Arrayクラス定数はその2進数表現で、数字が1の桁はすべて異なる(ように定められた)。したがって、すべての定数を足し合わせても繰り上がりは起こらない。そのため、加算しても論理和と同じ結果になる。

Arrayクラスの定数 2進数表現
CASEINSENSITIVE
0
0
0
0
1
DESCENDING
0
0
0
1
0
UNIQUESORT
0
0
1
0
0
RETURNINDEXEDARRAY
0
1
0
0
0
NUMERIC
1
0
0
0
0

02-04 ビット演算をどう使うか

オン/オフやYes/No、true/falseのような2値の状態を示す変数は「フラグ」と呼ばれる。フラグが複数あるとき、それらすべての値を確かめたいことがある。お題として、4つのフラグを初めはすべてオフにしておく。クリックするたびに、4つからランダムに選んだひとつのオン/オフを反転する。そして、すべてのフラグがオンになったかどうかを調べよう。

[1]複数フラグを配列で扱う

フラグ用の変数を複数設けるのは煩わしい。配列かVectorオブジェクトに、ブール(論理)値のエレメントを複数入れる方が扱いやすいだろう。初期値はfalseとしておく。

var flag:Array = [false, false, false, false];

値をランダムに反転する処理はさておく。すべてのエレメントの値がtrueになったかどうかは、forループで確かめられる。つぎの関数(xAllOn())は、配列エレメントがすべてtrueかどうかをブール値で返す。

function xAllOn():Boolean {
  var nLength:uint = flag.length;
  for (var i:uint = 0; i < nLength; i++) {
    if (flag[i] == false) {
      return false;
    }
  }
  return true;
}

もっとも、関数内のforループは必ずしもすべての配列エレメントを調べない。ひとつでもfalseのエレメントがあれば、ただちに関数の処理を終えてfalseを返している。

このような配列を使った複数フラグの扱いは定番といえる。もちろん、Vectorオブジェクト(Booleanベース型)を用いれば、より最適化される。しかし、ビット演算で組立て直すと、さらに速くできる。

[2]複数フラグをビットで扱う

ビット単位の演算は、2進数の各桁を分けて扱う。すると、それぞれの桁は1と0を値とするフラグとみなせる。

つまり、4つのフラグは4桁の2進数として表せる。すべてがオフの初期値を0 = 0000(2)とすれば、すべてがオンの値は15 = 1111(2)になる。

var flag:uint = 0;   // 2進数0000
var end:uint = 15;   // 2進数1111

すべてのフラグがオンになったかどうか返す関数は、たった1行のステートメントで済む。ループ処理など要らない。

function xAllOn():Boolean {
  return (flag == end);
}

ふたつの数値を比べて、等しいかどうかのブール値を返すだけだ。配列のループ処理より手間が少ないことは、説明の必要もないだろう。

[3]指定したビットの値を反転する

ランダムな桁のビットを反転する演算の仕方にも触れておこう。まず、ビットの反転にはビット単位の排他的論理和演算子^を使う。ビット単位の論理和と演算がひとつだけ異なり、1同士の演算結果は0になる(表004)。

表004■ビット単位の排他的論理和演算子^の演算結果
ビット単位の論理和演算 結果の2進数
0 ^ 0 0
0 ^ 1
1 ^ 0
1
1 ^ 1 0

表004の演算結果から^演算子の右側が1の場合を取出すと、つぎのように演算される左側のビットが反転する。つまり、ビットを反転させるには、1との排他的論理和を求めればよい。

0 ^ 1 1 反転
1 0

あとは、1のビットを動かすだけだ。これがビット単位のシフト演算で、左シフト<<右シフト>>がある。演算子の左側に対象とする整数、右側に動かす桁数を書く。

4桁のビットからランダムなひと桁を反転する処理はつぎのようになる。

var nRandom:uint = uint(Math.random() * 4);
flag ^= (1 << nRandom);

以上をテスト用にまとめたのが、つぎのスクリプト001だ。ステージをクリックするたびに、4桁のビットのうちランダムなひと桁が反転して[出力]パネルに示される(図003)。4桁すべてが1になると、"congratulations!!"という表示で終了する。

スクリプト001■ステージをクリックするたびに4桁のランダムなビットが反転する

// タイムライン: メイン
// 第1フレームアクション
var flag:uint = 0;
var end:uint = 15;
var nLength:uint = 4;
stage.addEventListener(MouseEvent.CLICK, xRandomRotate);
function xRandomRotate(eventObject:MouseEvent):void {
  var nRandom:uint = uint(Math.random() * nLength);
  flag ^= (1 << nRandom);
  trace(("000" + flag.toString(2)).substr(-nLength));
  if (xAllOn()) {
    stage.removeEventListener(MouseEvent.CLICK, xRandomRotate);
    trace("congratulations!!");
  }
}
function xAllOn():Boolean {
  return (flag == end);
}

図003■4桁のビットからランダムなひと桁が反転して[出力]される
図003

複数の値をまとめてひとつに扱えるのが、配列やVectorオブジェクトだった。けれど、各値がフラグのような2値であれば、ビット演算を用いることにより、処理は桁ごとに分けながら、まとめた結果をひとつの数値として扱うことができる。

[*6] 10進数はint.toString()メソッドで2進数の文字列に換えられる。引数には基数2を渡す。

var n:int = 11;
var n_str:String = n.toString(2);
trace(n_str);   // 出力: 1011

[*7] Array.sort()メソッドに引数を渡さないデフォルトでは、エレメントは頭からひと文字ずつ比べてソートされる。

[*8] 条件のAND(論理積)&&と同じ考え方なのが、ビット単位の論理積演算子&。演算対象(オペランド)が両方1なら1、そうでなければ0になる(表00)。

表00■ビット単位の論理積演算子&の演算結果
ビット単位の論理和演算 結果の2進数
0 & 0 0
0 & 1
1 & 0
0
1 & 1 1

03 ビットマップをキャッシュする

03-01 ベクターよりビットマップの方が描画は速い

  • ベクターグラフィックス
    • 数学的なデータ→サイズは小さい
    • ビットマップに変換(ラスタライズ)して描画→CPU負荷が高い

  • ビットマップグラフィックス
    • ピクセルごとのデータ→サイズは大きい
    • ピクセルをスクリーンに映す→CPU負荷が低い

ベクターグラフィックスの素材をビットマップグラフィックスにすると、アニメーションは速められる。とくに、フィルタは予め適用できると吉。

03-02 平行移動のアニメーションではDisplayObject.cacheAsBitmapプロパティをtrueにする

インスタンスのDisplayObject.cacheAsBitmapプロパティtrueにすると、表示用のビットマップが内部的につくられてキャッシュされる(デフォルト値はfalse)。

図004■数多くのインスタンスが下に移動し続けるアニメーション
図004

var _mc:MovieClip = new MyClass();
addChild(_mc);
_mc.cacheAsBitmap = true;

ただし、回転や伸縮などの変形、あるいはアルファやブレンドといったカラー演算があると役に立たない。むしろ、メモリを無駄遣いしてしまう。

03-03 パターンのかぎられる変形ならビットマップのキャッシュをつくってしまう

回転のアニメーションなら変化は1周360度にかぎられる。そういう場合は、必要なビットマップを予めつくっておき、アニメーションではイメージを差替えれば速さが稼げる[*9]

【BitmapData.draw()メソッドでイメージをコピーする】
BitmapDataオブジェクト.draw(コピーするインスタンス, 変形のMatrixオブジェクト)

ビットマップはBitmapDataオブジェクトでつくる。インスタンスのイメージは、BitmapData.draw()メソッドでコピーできる。回転などの変形は、メソッドにMatrixオブジェクトを渡して設定。

【ビットマップキャッシュ作成】

var rotationData:Vector.<BitmapData> = new Vector.<BitmapData>();
for (var i:uint = 0; i < 360; i++) {
  var myBitmapData:BitmapData = new BitmapData(幅, 高さ, true, 0x0);
  var myMatrix:Matrix = new Matrix();   // Matrixクラスで変形
  myMatrix.rotate(i / 180 * Math.PI);
  myBitmapData.draw(my_mc, myMatrix);
  rotationData[i] = myBitmapData;
}


【インスタンスの回転】

Bitmapインスタンス.bitmapData = rotationData[角度];

図005■予めVectorオブジェクトに入れておいたビットマップを切替えて回転させる
図005

[*9] ビットマップをキャッシュする方法やサンプルについては、以下を参照。


作成者: 野中文雄
更新日: 2012年2月8日 gihyo.jp連載第54回「【特別編】配列の処理をVectorオブジェクトで最適化する」へのリンクを追加。
作成日: 2012年2月6日
ドラフト作成: 2012年1月24日


Copyright © 2001-2012 Fumio Nonaka.  All rights reserved.