必ずしも速くないAVM2のint/uint型

ActionScript3.0になって、それまで数値は全てNumber型であらわされていたものが、int型とuint型で型指定できるようになった。この、int/uint型の利点としてよく言われることの一つとして高速化がある。

ためしにこういうコードを書いて実験してみると確かに速い。

var t:Number = getTimer();
var cnt:Number;
for(var i:Number=0;i<1000000;i++){
	cnt++;
}
var temp:Number = (getTimer()-t);

結果:20 ms

var t:Number = getTimer();
var cnt:int;
for(var i:int=0;i<1000000;i++){
	cnt++;
}
var temp:Number = (getTimer()-t);

結果:8 ms

ところがである。どうもそうは問屋が卸さないらしい。

Sho Kawamoto氏によれば、以下のようなコードを試すとint型よりもNumber型の方が速いらしい。

for (i=0; i<10000000; i++)
	j = (j + 15) / 7;

On my machine, the int version takes 331ms, while the Number version takes 291ms.

さらに、Grant Skinner氏の詳しい調査によると、なんとuint型がNumber型よりも遅い、という結果になったという。

もっとも、このテストはまだFlex2がベータの時の実験結果のようなので、正式版の今となっては必ずしもそのまま鵜呑みにできない結果である。そこで自分でも同様のテストを行ってみた。

テストコード

public function testFunc():void{
	var i:int;
	var j:int;
	var t:Number;
	var n:Number;
	
	//int 
	t = getTimer();
	for(j=0;j<10;j++){
		for(i=0;i<16777215;i++){var a:int=[code]; }
	}
	n = getTimer()-t;
	n /= 10;
	this.outputStr += "[int](int):"+n.toString()+"\n";
	
	//uint
	t = getTimer();
	for(j=0;j<10;j++){
		for(i=0;i<16777215;i++){var b:uint=[code];}
	}
	n = getTimer()-t;
	n /= 10;
	this.outputStr += "[int](uint):"+n.toString()+"\n";
	
	//Number
	t = getTimer();
	for(j=0;j<10;j++){	
		for(i=0;i<16777215;i++){var c:Number=[code];}
	}
	n = getTimer()-t;
	n /= 10;
	this.outputStr += "[int](Number):"+n.toString()+"\n";
	
	// *
	t = getTimer();
	for(j=0;j<10;j++){
		for(i=0;i<16777215;i++){var d:*=[code];}
	}
	n = getTimer()-t;
	n /= 10;
	this.outputStr += "[int](*):"+n.toString()+"\n";
}

for文を16777215回まわし、その中でローカル変数a,b,c,dを宣言し、ループイテレータ変数(i)に対していくつかの計算をして代入している。a,b,c,dはそれぞれ、int型、uint型、Number型、**1型の変数として宣言した。さらに、このfor文を10回行い、10回の平均値を求めた。

結果

結果は以下のようになった。

  • [int]はループイテレータ変数(i)の型を示している
  • テストの計算式はGrant Skinner氏のコードを参考にした
  • 減算がなかったのでi-1i-2を追加した
  • mxmlc:Version 2.0 build 143452,-optimize=trueでコンパイル
  • マシン:PentiumM 1.1GHz, OS:WinXP Pro SP2
var v:TYPE = i/2;

[int](int):634.3
[int](uint):618.2
[int](Number):216.5
[int](*):2313.3

                                                        • -
var v:TYPE = i*2; [int](int):633.4 [int](uint):625.2 [int](Number):212 [int](*):745.8
                                                        • -
var v:TYPE = i+2; [int](int):179.1 [int](uint):178.5 [int](Number):226.2 [int](*):743.4
                                                        • -
var v:TYPE = i<<1; [int](int):249.2 [int](uint):241.8 [int](Number):214.1 [int](*):285.8
                                                        • -
var v:TYPE = i-1; [int](int):167.4 [int](uint):161.8 [int](Number):227.5 [int](*):740.8
                                                        • -
var v:TYPE = i-2; [int](int):184.7 [int](uint):646.4 [int](Number):235.1 [int](*):764

分析

  • *型は予想通りどのテストでも遅い
  • 加算はint/uintの方がNumberよりも速い(但し、50ms程度)
  • 乗算・除算はint/uintの方がNumberより約3倍遅い
  • ビットシフトはint/uintの方がNumberより遅い(但し、30ms程度。どの型もあまり差がない)
  • 減算は興味深い結果
    • i-1の時はint/uintの方がNumberよりも速い
    • i-2の時はintとNumberの差はあまり変わらないが、uintが遅い(intの約3倍)

これから判断すると、uintがintよりもいつも遅くなるという仕様は正式版では改められてはいるが、場合によってはuintが遅い時があるようだ。また、「乗算・除算はint/uintの方がNumberより遅い」というのはやはり同様のようだ。

Sho Kawamoto氏の解説によれば、

virtually all math is done internally as Number, not as int.

つまり内部的には数学演算は全てNumber型で処理しているのでこういう結果になるらしい。

正しい型指定 vs. 最適化

Sho Kawamoto氏は以上のような結果からint/uint型ではなくNumber型をAS2の時と同じように使うことを薦めている。それに対し、以下のような反論もある。

16777215回も回して数十msの差なら大した問題ではない、と。それよりも適した正しい型指定をする方が重要だ、と。

たしかに、16777215回も回すfor文があったら、型指定云々の前にまずアルゴリズムを見直すべきだ。コンパイラやAVM2の仕様が変わったら、こういう最適化Tipsは役に立たなくなる可能性もある(もっとも、型指定まわりの仕様がそうそう変更されるとも思わないが…)。実際問題として、Flashが重いのは描画処理がかなりの原因であり、レンダリングエンジンはFP8とFP9ではほぼ同じでAVM1/AVM2でも共通なので、最適化はコードよりも描画部分に力を入れるべきだろう。

そんなことを考えながら以下の記事を読むとなおいいかもしれない。

ちなみに、ループイテレータ変数(i)は正しい型という意味でも、パフォーマンス最適化という意味でもint型にすべきのようだ。

*1:アスタリスク、全ての型を表す