アクセシビリティ
デベロッパーリソース
Fumio Nonaka

野中 文雄氏

Fumio Nonaka

http://www.fumiononaka.com/
作成日:
2008年2月12日
ユーザレベル:
中級
製品:
Adobe Flash

ActionScript 3.0におけるパフォーマンス向上のヒント

目次

サンプルファイルのダウンロード

Flash CS4 Professional

はじめに

本稿は、ActionScript 3.0のスクリプティングで、パフォーマンスを高めるテクニックについて解説します[*1]。ActionScript 3.0は、最適化されたAVM2(ActionScript Virtual Machine 2)で動作します[*2]。そのパフォーマンスを引出すポイントからスタートし、さまざまな小ネタをアラカルトで紹介します。内容の多くは、ActionScript 2.0でも活用できるでしょう。

ときには、処理スピードは、可読性やメンテナンスのしやすさとトレードオフになります。また、短いコードが速いともかぎりません。そうした注意点も確認します。さらに、Flash Player 10に加わった新たなクラスにも触れます。

[*1] 本稿は、2009年1月29日から30日にわたって催された「Adobe MAX Japan 2009」における筆者の講演「ActionScript 3.0におけるパフォーマンス向上のヒント」をもとに、基本説明や補足を加えて、新たにデベロッパーセンター向けの記事として書いたものです。

[*2] AVM2(ActionScript Virtual Machine 2)について詳しくは、gihyo.jp連載「ActionScript 3.0で始めるオブジェクト指向スクリプティング」の第1回「Flash Player 9とActionScript 3.0」の「Flash Player 9とは」をお読みください。

01 データ型を指定する

AdobeのMatt Chotin氏が米国AdobeサイトのDeveloper Centerに「Tips for tuning ActionScript 3.0 performance for Flex and Flash developers」という記事を寄稿しています。そのプレゼンテーションの中の1ページに、ActionScript 3.0の処理を最適化するためのポイントが挙げられています[*3]。それが下図001です。

図001 型指定がバイトコードを最適化する!

図001

訳すと「型指定がバイトコードを最適化する!」。バイトコードの最適化というのは、ActionScriptの処理のパフォーマンスが高まることを意味する。

バイトコード」というのは、ActionScriptからSWFファイルに書出されてFlash PlayerのAVMにより解釈・実行される命令(プログラム)のことです。つまり、バイトコードが最適化されるというのは、Flash Player上におけるActionScriptの処理効率が高まることを意味します。その鍵になるのが、一にも二にも型指定だというのです。

変数や関数に対するデータ型の指定の仕方を、簡単に確認しておきます。まず、変数はvar宣言時に、変数の後のコロン(:)に続けてデータ型を記述します[*4]

var 変数:データ型;
変数 = 値;

変数の宣言と初期値の代入は、1行のステートメントで済ませることもできます。

var 変数:データ型 = 値;

つぎに関数は、引数にコロン(:)をつけて、データ型を指定します。また、関数呼出しの括弧()の後にコロン(:)をつけると、戻り値の型指定ができます[*5]

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

ActionScript 3.0でデータ型を指定すると、おもに3つの利点があります。

【型指定の利点】

  1. 処理が最適化される。
  2. 型チェックによるエラーが返される。
  3. コードヒントが表示される。

第1は、本稿のテーマでありChotin氏も強調していた処理が最適化されることです。AMV2が型の情報を用いてデータを扱うため、パフォーマンスが高まり、使われるメモリも少なく済みます[*6]。第2に、指定したデータ型にもとづくエラーの確認が行われます。データ型はまずコンパイル(SWF書出し)時にチェックされ[*7]、さらにランタイム(SWFの実行)時にもエラーが検出されます。第3に、型指定した変数には、コードヒントが表示されます[*8]

ただし、ActionScript 2.0では、型指定の情報がSWFに書出されません。ランタイム時のActionScriptの処理は型指定のない1.0と異なるところがなく、バイトコードも基本的に同じです。データ型のチェックも、コンパイル時のみということになります[*9]

ActionScript 3.0で型指定による最適化の恩恵を受けるには、「強い参照」(strong reference)が必要です[*10]。それは、型指定とドットシンタックスにより得ることができます。型指定をしなかったり、配列アクセス演算子[]でプロパティにアクセスすると「弱い参照」(weak reference)になります(表001)。

表001 強い参照と弱い参照

必要な条件 強い参照 弱い参照
データ型 指定する 指定しない
シンタックス
記述方法
ドットシンタックス
オブジェクト.プロパティ
my_mc.x
配列アクセス
オブジェクト[プロパティ名]
my_mc["x"]

たとえば、整数を格納するaというプロパティが納められたオブジェクトは、Objectクラスを使えば簡単に生成することができます。しかし、プロパティaには、データ型が指定できません。つまり、整数以外のどのようなデータでも代入されてしまいます。

var myObject:Object = new Object();
myObject.a = 0;
// myObject.a = "test";   // どのようなデータでも代入できる

そこで、少し面倒でもそのためのクラスを定義すると、プロパティのデータ型が指定できます。さらに、型指定により処理が最適化されるので、プロパティへのアクセスは速くなります。たとえば、整数を格納するaというプロパティが備わったクラスMyObjectは、つぎのように定義すればよいでしょう(スクリプト001)[*11]

スクリプト001 int型のブロパティをもったクラスの定義

// ActionScript 3.0クラス定義ファイル: MyObject.as
package {
  public class MyObject {
    public var a:int;
    // コンストラクタメソッドの定義省略[*12]
  }
}

フレームアクションでクラスMyObjectのインスタンスを生成して、そのint型のプロパティaに文字列(String型)の値を代入しようとすれば、Objectクラスの場合とは異なり、[コンパイルエラー]が生じます(図002)。

図002 クラス定義で型指定したプロパティに異なるデータ型の値を入れようとするとコンパイルエラー

図002

クラスにint型で宣言したプロパティに、フレームアクションから文字列の値を入れようとすると、[コンパイルエラー]が生じる。

[*3] プレゼンテーションのデータは、前出「Tips for tuning ActionScript 3.0 performance for Flex and Flash developers」からPDFファイルでダウンロードできます。

[*4] 変数の型指定について詳しくは、前出注[*2]「ActionScript 3.0で始めるオブジェクト指向スクリプティング」の第3回「変数を使う」の「変数の使い方」をお読みください。

[*5] 関数への型指定について詳しくは、前出注[*2]「ActionScript 3.0で始めるオブジェクト指向スクリプティング」の第5回「イベントリスナーを使う」の「addEventListener()メソッド」および同じく第8回「Stringクラスによる文字列の操作と値を返す関数」の「値を返す関数の定義」をご覧ください。

[*6] データ型の指定による処理の最適化については、Adobeデベロッパーセンター「ActionScript 3.0の概要」(「ActionScript 3.0 overview」)の「言語の機能」(「LANGUAGE FEATURES」)およびマイコミジャーナル「ActionScript、Flex、そしてApollo - Flexエバンジェリストに訊くAdobe開発ツールの現在」の「型指定はパフォーマンスを最適化する」をご参照ください。

[*7] コンパイルエラーは、デフォルトの[Strictモード]で表示されます([Flash CS4 Professionalユーザガイド] > [ActionScript] > [ActionScript 3.0 のデバッグ]の「コンパイラ警告の制御」を参照)。

[*8] コードヒントの表示の仕方については、「コードヒントの活用」をご覧ください。

[*9] ActionScript 2.0と1.0との動作の違いについては、「ActionScriptとその基本概念について」の07「ActionScript 1.0 vs 2.0」をご参照ください。

[*10] 強い参照は、C++のメンバアクセスに匹敵します(前出注[*6]に引用したドキュメントを参照)。

[*11] クラスの定義について詳しくは、前出注[*2]「ActionScript 3.0で始めるオブジェクト指向スクリプティング」の第18回「カスタムクラスを定義する」をご覧ください。

[*12] コンストラクタメソッドを定義しなければ、コンパイラが自動的に空のコンストラクタを作成します(前出注[*11]「カスタムクラスを定義する」の注※1参照)。

02 型指定した変数を活用する

01「データ型を指定する」に述べたとおり、変数に型指定をすると処理が最適化されます。たとえば、ひとつの処理の中で何度もオブジェクトのプロパティにアクセスするときは、型指定した変数を活用できることが少なくありません。とくにアクセス回数が増えやすいforループの繰返し処理を例に取ります。

配列エレメントを処理する

forループで扱うオブジェクトの典型は、配列でしょう。配列(my_array)のエレメントすべてを取出して処理しようとするとき[*13]、参考書ではつぎのような例を紹介していることがあります。

for (var i:int = 0; i<my_array.length; i++) {
  // 配列エレメントmy_array[i]に対する処理
}

この記述でも、ことさら問題はありません。また、forステートメントを使った文法の説明としては、これでもよいでしょう。

けれど、最適化の観点からは、改善の余地があります。なぜなら、継続条件はループするたびに毎回確かめられるからです。つまり、Array.lengthプロパティの値が、ループする回数分だけ参照されます。だとすれば、この値は予めつぎのスクリプト002のように、型指定した変数(nLength)に納めておく方がよいのです[*14]

スクリプト002 配列のループ処理ではArray.lengthプロパティの値は予め型指定した変数に納めておく

var nLength:uint = my_array.length;
for (var i:int = 0; i<nLength; i++) {
  // 配列エレメントmy_array[i]に対する処理
}

なお、forステートメントのカウンタ変数(i)は通常整数を用いますので、浮動小数値(Number型)でなく整数(intまたはuint型)で指定する方が処理は速くなります。

TextFieldインスタンスに文字列を加える

ActionScript 3.0では、TextField.textプロパティに加算後代入演算子+=で文字列を追加しようとすると、デフォルトでは以下の警告(Warning)が表示されます(図003)[*15]。演算子+=でなく、TextField.appendText()メソッドを使うよう促されるのです。

図003 TextField.textプロパティに演算子+=でテキストを追加すると警告が表示される

図003

加算後代入演算子+=でなく、TextField.appendText()メソッドを用いるよう促される。

では、forループでTextFieldインスタンスに、文字列を連続して加える場合について考えてみます。テストとしてたとえば、0から9までの数字をTextFieldインスタンスに続けて追加するなら、forステートメントをつぎのように記述すればよいでしょうか。

for (var i:int = 0; i<10; i++) {
  test_txt.appendText(String(i));
}

確かに、加算後代入演算子+=を用いるよりは、ずっと処理は速くなります。けれど、TextFieldインスタンスに文字列を設定する操作そのものが、そもそも負荷の高い処理です。forステートメントで繰返すコードブロックからは、できるだけ重い処理を外す工夫が求められます。

この場合でしたら、繰返す文字列の連結にはStringで型指定した変数を用い、forループが終わってからその変数の文字列をTextFieldインスタンスに加えればよいでしょう(スクリプト003)[*16]TextField.appendText()メソッドと比べて、String型の変数に文字列を追加する処理はずっと軽くなります。

スクリプト003 String型の変数にループ処理で文字列を加えた後TextField.appendText()メソッド使用

var test_str:String = "";
for (var i:int = 0; i<10; i++) {
  test_str += String(i);
}
test_txt.appendText(test_str);

[*13] 整数0から連続したn個の自然数のエレメントが納められた配列(my_array)は、以下のような短いスクリプトで生成することができます。forステートメントに続く中括弧{}で括られた繰返し処理のコードブロックがなく、セミコロン(;)でステートメントが終わっていることにご注目ください。Array.pus()メソッドの戻り値により、配列のエレメント数を調べています。

var nCount:uint = 10;
var my_array:Array = new Array();
for (var n:int = 0; my_array.push(i)<nCount; i++);
trace(my_array);   // 出力: 0,1,2,3,4,5,6,7,8,9

もっとも、本文に述べる理由から、この処理は効率的とはいえません。

[*14] ActionScript 2.0または1.0でも、ローカル変数はタイムラインに宣言した(タイムライン)変数よりアクセスが速くなります。したがって、関数内で配列エレメントをforループで処理する場合には、Array.lengthプロパティの値はローカル変数に納めておく方がよいでしょう。

[*15] 警告(Warning)のコードは、3551と示されます。しかし、ヘルプの[コンパイラ警告]には3552と記載されています。これはドキュメントの誤りでしょう。

[*16] String型を指定した変数には必ず初期値、とくに値がなければ空文字列""を設定しましょう。String型変数のデフォルト値はnullですので、何も指定しないと文字列表現として"null"に変換されてしまいます(図004)。

図004 String型変数のデフォルト値はnull

図004

初期値が設定されていないString型変数を文字列として扱うと、"null"という文字列に変換される。

03 条件判定を考える

使うプロパティやメソッド、演算子などを変えることによる処理速度の違いというのは、後述のとおり環境によりかなりばらつきがあります。もちろん、Flash Playerがアップデートされれば、結果が変わることは十分あり得ます。そう考えると、もっとも確実な最適化というのは、基本に戻って処理手順つまりアルゴリズムを磨くことだといえます。ここでは、条件判定の処理を採上げます。

たとえば、「うるう年かどうかを判定する関数」はどのように定義したらよいでしょうか。引数に渡した整数の年がうるう年であればtrue、そうでなければfalseを返す関数です。うるう年は、つぎのように定められています。

【うるう年の判定方法】

  1. 4で割り切れる年はうるう年。
  2. ただし例外として、100で割り切れる年は普通の年。
  3. さらに例外の例外として、400で割り切れる年はうるう年。

すると、うるう年を判定する関数isLeapYear()は、条件に論理積&&や論理和||などの演算子を用いて、以下のスクリプト004のように定義されるかもしれません。この関数は、うるう年を正しく判定します(図005)。なお、剰余演算子%は、右側の項(オペランド[*17])で割った余りを求めます。

スクリプト004 複数の条件を組合わせて判定する

function isLeapYear(nYear:int):Boolean {
  if ((nYear%4 == 0 && nYear%100 != 0) || nYear%400 == 0) {
    return true;
  } else {
    return false;
  }
}

図005 複数の条件をifステートメントに指定

図005

関数はうるう年を正しく判定。ただし、条件がわかりくい。

しかし、いくつもの条件を組合わせると複雑になりがちで、誤りを生みやすくなります。また、最適化を考えるにも、分析しにくいでしょう。ifステートメントは、else ifelseと組合わせれば、条件判定を構造化することができます。if/else if/elseステートメントにより複数の条件を指定すると、それらが順に判定されます。そして、最初にtrueと評価されたコードブロックを実行したら、ただちに処理を抜けます。

いわば最近テレビ番組で流行っている勝抜けクイズのようなものです。1問でも正解すれば、その人はその場で解答者席から抜け、あとの問題には答える必要がありません(図006)。条件判定も、ひとつtrueと評価されれば処理を抜けて、あとの条件については問われないということです。すると、条件の順序が大切になってきます。

図006 条件判定の処理は勝抜けクイズと同じ

図006

イラスト: AYA

1問でも正解すれば勝ち抜けで、あとの問題には答えない。

収穫したみかんを大きさによって仕分けるみかん選別機は、コンベヤーの先に大きさの異なる穴が空いていて、その落ちた穴によってSMLが分けられるそうです(図007)。そのとき、間違っても一番手前にLの穴を空けていけないことは、すぐにわかるでしょう。手前にSの穴があれば、まず小さいみかんがすべてその穴から落ちます。すると、その先は穴を通らなかったMとLとの選別になります。したがって、つぎにMの穴、そして最後にLの穴という順にすれば、正しく仕分けられます。

図007 みかん選別機の穴は手前から順にSML

図007

イラスト: AYA

手前ほど穴を小さくしないと、正しく仕分けできない。

うるう年の判定に戻りましょう。条件は例外から考えると、整理しやすいことが少なくありません。つまり、例外の例外である400で割切れる場合から順に考えます(スクリプト005)。

スクリプト005 例外から順に判定する

function isLeapYear(nYear:int):Boolean {
  if (nYear%400 == 0) {   // 400で割り切れる
    return true;   // 例外の例外のうるう年
  } else if (nYear%100 == 0) {   // 100で割り切れる
    return false;   // 例外の普通の年
  } else if (nYear%4 == 0) {   // 4で割り切れる
    return true;   // うるう年
  } else {   // 残り
    return false;   // ごく普通の年
  }
}

400で割切れれば、迷うことなくうるう年です。すると、400で割切れる年はこの条件で勝抜けます。よって、つぎの条件の100で割切れるというのは、400で割切れない数のうち100で割切れるということですから、例外で普通の年になります。これで、例外はすべて勝抜けました。あとは4で割切れるかどうかにより、うるう年と普通の年とを仕分けるだけです。

みかん選別機を思い浮かべながら見ていけば、処理の流れが理解しやすいでしょう。もっとも、この仕分け方は、あまり効率のよいものではありません。最初に勝抜けるのは、400年に1度のうるう年です。つづけて、100年に1度の例外的な普通の年が抜け、ざっと3/4を占めるであろうごく普通の年は最後まで残ってしまいます。つまり、普通の年たちは、3問すべてに回答しなければならないということです。

処理効率を高めるには、できるだけ初めの方の問題で、より多くの勝抜けを出すべきです。そのためには、つぎのスクリプト006のように、条件を少しひねる必要があります。

スクリプト006 初めに多くの勝抜けを出す

function isLeapYear(nYear:int):Boolean {
  if (nYear % 4 != 0) {   // 4で割り切れない
    return false;   // ごく普通の年
  } else if (nYear%100 != 0) {   // 100で割り切れない
    return true;   // うるう年
  } else if (nYear%400 != 0) {   // 400で割り切れない
    return false;   // 例外の普通の年
  } else {   // 残りは400で割り切れる
    return true;   // 例外の例外のうるう年
  }
}

条件を否定形にしているので、少しわかりにくいかもしれません。けれど、選別機の考え方は同じで、仕分けを前掲スクリプト005とはちょうど逆の順序にしているだけです。スクリプト005よりはもちろんスクリプト004と比べても、処理の効率は高まっています。

もっとも、スクリプト004のように複数条件を論理演算子&&||で組合わせる場合にも、最適化は考えられます。評価が論理演算子の左辺(左オペランド)だけで判定できる場合、右辺(右オペランド)は評価されません。

if ((nYear%4 == 0 && nYear%100 != 0) || nYear%400 == 0) {

ですから、スクリプト004におけるif条件の最初の論理式「nYear%4 == 0 && nYear%100 != 0」は、関数の引数nYearが4の倍数でなければ、論理積演算子&&の右オペランドである「nYear%100 != 0」は評価することなくfalseを返します。そして、つぎの論理演算子が論理和||ですので、その右オペランド「nYear%400 == 0」がつぎに評価されることになります。

また、逆に引数nYearが4の倍数であれば、つぎに論理積演算子&&の右オペランドが評価されます。つまり、スクリプト004の条件の組合わせでは、少なくともふたつ以上の論理式を評価しなければ、条件判定ができないことになります。それに対してスクリプト006は、ひとつの論理式の評価だけで、3/4近くを占める普通の年を勝抜けさせてしまうのです。

[*17]「オペランド」とは、被演算子を意味します(「ドット演算子と配列アクセス演算子」の注釈[*1]参照)。

04 visibleとalphaとremoveChild()

ステージ上に表示されたインスタンスを画面から消すには、3つのやり方が考えられます[*18]。第1に、文字どおりインスタンスの表示・非表示を切替えるDisplayObject.visibleプロパティが使えます。第2に、アルファ値のプロパティDisplayObject.alphaを0に設定すれば完全な透明になり、インスタンスは画面から消え去ります。第3は、DisplayObjectContainer.removeChild()メソッドにより、インスタンスを表示リストから取除くことです。

【インスタンスを画面から消すプロパティとメソッド】

  1. DisplayObject.visibleプロパティ
  2. DisplayObject.alphaプロパティ
  3. DisplayObjectContainer.removeChild()メソッド

3つのいずれを使っても、インスタンスは画面から消えます。これら3つの手法の特徴と違いをまとめてみましょう。

DisplayObject.visibleプロパティ

インスタンスを表示しないようにするには、DisplayObject.visibleプロパティをfalseに設定するのが一番素直でしょう。非表示になったインスタンスは、画面の描画には負荷として加わりません。もとどおりに表示したいときは、プロパティ値をtrueに戻すだけですので簡単です。

注意しなければならないのは、インスタンスが表示リスト内には存在し続け、そのサイズの情報も残ったままだということです。たとえば、他のインスタンスとの当たり判定(DisplayObject.hitTestObject()メソッドの呼出し)をすれば、表示されていなくても判定の領域には含まれることになります。また、表示リスト内のインスタンスすべてを処理しようとすると、非表示のインスタンスも対象となります。

なお、DisplayObject.visibleプロパティをfalseに設定したインスタンスは、マウスイベントを受取りません。

DisplayObject.alphaプロパティ

インスタンスのアルファを0にすれば、完全に透明になり、画面からは消え去ります。もっとも、とくに必要がある場合以外には、DisplayObject.alphaプロパティを使って画面から消すことはお勧めしません。なぜなら、半透明のときとほどではないとはいえ、画面の描画負荷がかかるからです。

インスタンスが表示リスト内に存在し続け、サイズ情報に反映されることはDisplayObject.visibleプロパティと変わりません。違うのは、DisplayObject.alphaプロパティを0にしても、マウスイベントを受取ることです。つまり、透明ボタンができます。

もっとも、透明ボタンをつくるには、Sprite.hitAreaプロパティにヒット領域とするインスタンスを指定し、そのDisplayObject.visibleプロパティをfalseにしてしまう方法があります(図008)。このやり方であれば、描画負荷はかかりません。したがって、アルファを変化させるアニメーションに使う以外は、DisplayObject.alphaプロパティで画面から消さなければならない場合はあまりないでしょう。

図008 Sprite.hitAreaプロパティに非表示のインスタンスを指定する

図008

Sprite.hitAreaプロパティに、ヒット領域のインスタンスを非表示(DisplayObject.visibleプロパティをfalse)にして設定すれば、透明ボタンができる。

DisplayObjectContainer.removeChild()メソッド

DisplayObjectContainer.removeChild()メソッドを使うと、インスタンスはStageオブジェクトを頂点とする表示リストから完全に削除されます[*19]。描画に負荷もかからなければ、サイズの情報もなくなります。もちろん、マウスイベントなど受取りようがありません。

けれど、もとどおりに表示しようとすると、少々面倒です。インスタンスを表示リストから削除する前の情報が必要になります。たとえば、インスタンス同士の重ね順、つまり表示リスト内のインデックスは覚えておかなければならないでしょう。

表示・非表示を頻繁に切替える必要がなく、当分インスタンスが不要になるという場合に用いるのが適切だと考えられます。以上、3つの手法についてその違いをまとめたのが下表002です。

表002 画面からインスタンスの表示を消すプロパティとメソッド

操作・機能 visible alpha removeChild()
描画負荷 なし 少しあり なし
再表示 簡単 簡単 面倒
サイズの情報 残る 残る 残らない
マウスイベント 受取らない 受取る 受取らない
表示リスト内のインスタンス 存在する 存在する 存在しない

なお、3つのいずれのやり方でも、MovieClipインスタンスのフレームアニメーションは止まりません。無駄な処理をしないためには、原則として再生ヘッドは止めておくべきでしょう。

[*18] 3つの手法の比較について詳しくは、Colin Moock「The Official "visible vs alpha vs removeChild()" Showdown」をご参照ください。

[*19] インスタンスへの参照をすべて破棄しないかぎり、メモリには残りますので注意が必要です。

05 ArrayとVectorクラス

Arrayクラスの配列インスタンスは、複数の値を納めて扱うことができ、さまざまな場面で活用されます。そして、Flash Player 10では、配列と使い途の似たVectorクラスが実装されました[*20]。これらふたつのクラスについて、おもにパフォーマンスの観点から簡単に説明します。

Arrayクラス

Arrayインスタンスの扱いについては、第1に前述02「型指定した変数を活用する」でご説明したとおり、forステートメントですべてのエレメントを取出すときには、配列の長さであるArray.lengthプロパティの値は予め型指定した変数に取っておくべきでした。

第2に注意しなければならないのは、配列エレメントには型指定ができないことです。したがって、型指定による最適化の恩恵を受けるには、取出したエレメントの値は型指定した変数に入れる必要があります。

第3に、配列エレメントは密(dense)である方が、アクセスは速くなります[*21]。密というのは、エレメントがインデックス0から連番で納められていることを意味します。たとえば、つぎのようなArrayインスタンスを作成すると、インデックス0から2までは連番ですのでエレメントは密です。しかし、インデックス1000のエレメントは密ではありません。

var my_array:Array = new Array();
my_array.push(0);
my_array.push(1);
my_array.push(2);
my_array[1000] = 1000;

すると、インデックス0から2までのエレメントの方が、インデックス1000のエレメントより速くアクセスできます。

my_array[2]   // アクセスが速い
my_array[1000]   // アクセスは遅い

Vectorクラス

Vectorクラスのインスタンスには、配列と同じように複数の値を納めることができます。また、Arrayクラスと同じく、lengthプロパティやpush()pop()slice()sort()などのメソッドを備えています。

しかし、Vectorクラスはふたつの点で、Arrayクラスとは扱いが異なります。第1に、すべてのエレメントをひとつのデータ型で指定します。したがって、他の型のデータをエレメントにすることはできません。第2に、エレメントのインデックスは連番、つまり密でなければなりません。Vector.lengthプロパティの値より大きいインデックスには、エレメントが加えられません。

【配列と異なる点】

  1. エレメントにひとつのデータ型を指定。
  2. エレメントは密でなければならない。

Vectorクラスのコンストラクタは、つぎのようなシンタックスでインスタンスを生成します。

第1引数の長さには、エレメント数を指定します。デフォルトは0です。第2引数は、長さを固定するかどうかをブール(論理)値で渡します。デフォルトはfalseで、長さは固定されません。

Vectorクラスを使うとエレメントにデータ型が指定されるため、エレメントに値を設定するときだけでなく、取出した値についても指定した型が有効です。また、エレメントへのアクセスは、配列と比べて高速です。

Vectorインスタンスの生成とエレメントの値の追加、変更、取出しは、つぎのように行います。

var myVector:Vector.<int> = new Vector.<int>();
myVector.push(0);
myVector.push(2);
myVector[1] = 1;
// myVector[5] = 5;   // インデックスが長さを超えるためエラー
var n:int = myVector[0];
// var my_str:String = myVector[1];   // データ型が一致しないためエラー

エレメントのデータ型がひとつで、連番のインデックスに値を納めるときには、ArrayよりVectorクラスのインスタンスを使う方がよいでしょう。また、ActionScript 3.0に新たに備わるメソッドには、引数として複数の値をVectorインスタンスで渡す場合が増えると思われます[*22]。エレメントのデータ型が指定できるので、引数値に確実さが増すからです。

[*20] 併せて「Vectorクラス」をご覧ください。

[*21] 前出注[*3]「Tips for tuning ActionScript 3.0 performance for Flex and Flash developers」のPDFのp.14「Array Member Access」をご参照ください。

[*22] たとえば、Flash Player 10に実装されたGraphics.drawTriangles()メソッドは、つぎのように3つの引数をVectorインスタンスで指定します。

06 数値の演算

数値の演算について、処理速度が変わる例をいくつかご紹介します。ただし、数値演算にかぎらず、処理方法による速度の差は、環境によってばらつきのあることが少なくありません。オペレーティングシステム(OS)による違いはもちろん、[ムービープレビュー]とブラウザ、さらにブラウザの種類により、ときには結果が逆転することさえあります。

こうした違いは、FlashコンテンツがFlash Playerとブラウザを介して処理を行っていることに起因すると考えられます。したがって、これらのアップデートにより、また結果も変わってくると予想されます。ですから、03「条件判定を考える」にも述べたとおり、細かな差にかかずらうより、処理手順やそのロジックを重視した方がよいでしょう。

Mathクラスの数値演算

Mathクラスのメソッドの多くは、処理スピードがあまり速くないようです。たとえば、数値の2乗や絶対値の取得は、演算子で直接計算した方が速いという結果になります(表003)。

表003 画面からインスタンスの表示を消すプロパティとメソッド

演算 Mathクラスによる処理 速い計算式
数値(n)の2乗 Math.pow(n, 2) n*n
数値(n)の絶対値 Math.abs(n) (n<0) ? -n : n

もっとも、Mathクラスの名誉のためにひとこと添えるなら、演算子による計算式が直接記述されていることは速さのうえで有利になります。この計算式を関数として定義すれば、その呼出しに負荷が生じ、処理速度の差は縮まります。

もうひとつ、切捨ての処理を紹介しておきます。通常は、切捨てはMath.floor()メソッドを使って行います。しかし、数値を整数に変換するグローバル関数int()が、小数点以下の数値を同じように切捨てます[*23]。そして、int()関数の方が、Math.floor()メソッドよりも処理は速いです。

掛け算と割り算

ActionScript 3.0の数値演算では、割り算よりも掛け算の方が少し速いようです[*24]。たとえば、整数nに対するつぎのふたつの演算では、割り算より掛け算の方が速いという結果になります。

ただし、2の累乗による割り算はFlash Playerで最適化されているらしく、掛け算との差がほとんどなくなります。ですから、少しばかり速さに差があるからといって、割り算をすべて掛け算に書直す必要はないでしょう。

もっともたとえば、つぎのような座標のイージング(イーズアウト)の処理では、減速率のパラメータとして筆者は5で割るより0.2で掛け算する方を好みます。しかし、それは処理速度が理由ではありません。割り算にするとパラメータは反比例の係数になり、演算値が双曲線を描いて変わります。掛け算であれば比例の直線的な変化ですので、値の微調整がしやすいのです(図009)。

x += mouseX*0.2;

図009 比例と反比例のグラフ

図009

係数による演算値の変化が、比例は直線的で、反比例は双曲線になる。

ビット演算

ビット演算は1と0の2進数に基づく演算で、CPUの処理に近いため、一般に高速とされます。とくにビット単位の演算は、他の桁に影響を与えませんので、処理の最適化が期待できます。もっとも、実際の処理結果を比べてみると、環境による違いも少なくありません。

整数に対して2もしくは2の累乗で掛け算や割り算を行う場合、ビット単位のシフト演算を用いると速いといわれます[*25]。10進数で数値全体をひと桁繰り上げれば10倍になり、逆にひと桁繰り下げれば1/10になります。ビット単位のシフト演算はこれを2進数で行いますので、整数を2倍もしくは1/2にすることができます。

ビット単位の左シフト演算子<<は、左オペランドの整数(nNumber)を右オペランドの整数(n)の桁数左に繰上げます。したがって、2n(2のn乗)を掛け算したのと同じ結果になります。

nNumber << n

ビット単位の右シフト演算子>>は、左オペランドの整数(nNumber)を右オペランドの整数(n)の桁数右に繰下げます。したがって、2n(2のn乗)で割り算したのと同じ結果になります。

nNumber >> n

ビット単位のシフト演算は、一般的には2または2の累乗による掛け算や割り算より処理が速くなります。ただ、筆者のテストしたかぎりでは、環境によるばらつきが目立ちます[*26]

ビット単位の論理和演算子|は、ふたつのオペランドの対応する各ビット(2進数の各桁)の値の少なくとも一方が1であれば1を、そうでなければ0を返します。オペランドも0か1ですので、4とおりの組合わせが考えられ、ビット単位の論理和演算子|の演算結果は下表004のとおりです。

表004 ビット単位の論理和演算子|の演算結果

オペランド 0 1
0 0 1
1 1 1

Array.sortOn()メソッドの第2引数にソートオプションとして複数のArray定数を指定するとき、それらの定数は|演算子でまとめます。たとえば、配列my_arrayをフィールド名field_strで、数値の降順に並べ替えたいときは、つぎのようなステートメントを記述します。

my_array.sortOn(field_str, Array.DESCENDING | Array.NUMERIC);

Array定数は次表005のとおり、2進数で表したとき値1を取る桁が重ならないようにフラグとして定められています。したがって、複数の定数値をまとめる場合、論理和|でなく加算+で演算しても同じ結果が得られます。けれど、2進数の桁をフラグとして用いるのは、ビット演算の考え方にもとづくものです。ですから、ビット単位の演算子を使うのが一貫しているでしょう[*27]

表005 Array定数とその値

Array定数 10進数で表した値 2進数で表した値
CASEINSENSITIVE 20 = 1 0 0 0 0 1
DESCENDING 21 = 2 0 0 0 1 0
UNIQUESORT 22 = 4 0 0 1 0 0
RETURNINDEXEDARRAY 23 = 8 0 1 0 0 0
NUMERIC 24 = 16 1 0 0 0 0

ビット演算を用いる典型は、カラー値の計算です。カラー値はRGB各成分値を0からFまでの16進数ふた桁の256階調で指定し、計6桁で表します。つまり、16進数ふた桁をひとつとみなせば256進法ということになります。

16進法はひと桁が24(=16)ですから、ビット演算では4桁になります。したがって、16進法のひと桁繰上げ/繰下げは、ビット単位で4桁シフトすればよいでしょう。256進法のひと桁は16進数でふた桁(162 = 24×2 = 28)に当たりますので、ビット単位の8桁シフトになります(図010)。

図010 256進法(階調)のひと桁はビット単位の8桁になる

図010

イラスト: AYA

256進法のひと桁繰上げは、256(=28)を掛けることで、ビット単位の演算で8桁左シフトになる。

すると、RGBの各256階調の値がint型の変数nR、nG、nBにそれぞれ入っているとすれば、RGGカラー値はつぎのようなビット演算で求めることができます。

var nColor:int = nR << 16 | nG << 8 | nB;
trace(nColor, nColor.toString(16));   // 確認用

暗黙の型変換

forループのカウンタ変数について述べたとおり、整数の演算は変数をintまたはuint型で指定した方が処理は速くなります。しかし、だからといって、何でも整数で型指定すればよいという訳ではありません。たとえば、int型の変数にNumber型の浮動小数値を代入すれば、数値のデータ型が黙示的に変換されます。その処理が加わることにより、負荷はかえって増します。ですから、数値の型指定は適切に行う必要があります。

[*23] ActionScript 2.0/1.0では、int()はFlash Player 5以降の使用が推奨されない古い関数です。また、負の数の扱いが、Math.floor()メソッドやActionScript 3.0のint()関数とは異なりますのでご注意ください(F-site「int関数は整数を丸めない」参照)。

[*24] 詳しくは、「掛け算と割り算の処理速度」をお読みください。

[*25] ふたつのオペランドはNumber型で指定されています。しかし、戻り値はint型ですので、基本的には整数の演算を想定していると考えられます。

[*26] 詳しくは、前出注[*24]の「掛け算と割り算の処理速度」およびF-site「偶数を2で割る」をご参照ください。

[*27] 詳しくは、「複数のフラグをひとつの整数で表す」をお読みください。

著者について

野中 文雄 Fumio Nonaka
ソフトウェアトレーナー、テクニカルライター、オーサリングエンジニア
上智大学法学部卒、慶応義塾大学大学院経営管理研究科修士課程修了(MBA)。 独立系パソコン販売会社で、総務・人事、企画、外資系企業担当営業などに携わる。その後、マルチメディアコンテンツ制作会社に転職。
ソフトウェアトレーニング、コンテンツ制作などの業務を担当する。2001年11月に独立。
Web制作者に向けた情報発信プロジェクトF-siteにも参加する。株式会社ロクナナ取締役(非常勤)。
近著は『ActionScript 3.0プロフェッショナルガイド』(株式会社毎日コミュニケーションズ)。