Bench.as

2008.7.12

普段使っている簡単なベンチマーク用のクラスを晒してみます。この手のやつってみんな自作のものを持っているか、もっといいものがあると思いますが、こういうやり方もあるということで・・。

早速使い方ですが、計測したい部分を以下のようにbeginとendで囲みます。

var ary:Array = [];

Bench.begin("loop");
for (var i:uint = 0; i < 500; ++i) {
  var sum:uint = 0;

  Bench.begin("subloop");
  for (var j:uint = 0; j < i; ++j) {
    Bench.begin("add");
    sum += j;
    Bench.end();
  }
  Bench.end();

  ary.push(sum);
}
Bench.end();

/*
                 time       rate      count
loop              830      1.000          1
subloop           826      0.995        500
add               223      0.269     124750
*/

そうすると、コメントにあるような表がトレースされます。表の1列目はbeginの引数に与えたラベル、2列目がミリ秒単位の処理時間、3列目が処理時間の割合、4列目が呼び出された回数になります。beginの引数を省略すればその部分の結果は出力しないようにもできます。また、ネストした場合、トップレベルのベンチマークが終わったときにまとめて出力されるので、他のtrace文が入っていても結果が混ざって見にくくなる心配はありません。

これにはもう一つ使い方があって、timesというメソッドにコールバック関数と回数を指定して計ることもできます。endやtimesは計測した時間を返すので、必要であれば以下のようにしてどの程度パフォーマンスが改善されたかを求めることができます。

function func() {
  var a = 1, b = "", c = {};
  a + b + c;
}

function typedFunc():void {
  var a:Number = 1, b:String = "", c:Object = {};
  a + b + c;
}

var count:uint = 100000;
Bench.times(null, function():void {
  var t0:uint = Bench.times("before", func, count);
  var t1:uint = Bench.times("after", typedFunc, count);
  trace(Math.round(100 * (t0 / t1 - 1)) + "%の高速化");
});

/*
39%の高速化
                 time       rate      count
before            759      0.582          1
after             545      0.418          1
*/

もし、ラベル名が長くて表が崩れてしまうときは、列の横幅を大きめに設定しておくこともできます。

Bench.columnWidth = 20;

ソースですが、ブログに貼るとなんか記号が化けるのでこちらから。

追記:
いくつか気になる点があったので、本文中の例とソースを以下のような修正版に差し替えました。

  • ループ等で同じラベルを持つ計測が複数回行われた場合、表示を1行にまとめるようにした。
  • 出力する表にカラム名を加えた。
  • 出力する表に呼び出し回数の列を加えた。
  • 無名関数を指定した後にラベルが続くと見にくいので、timesの引数の順序を変えた。

さらに追記:
出力する表のソートキーを選べるようにしました。Bench.orderというプロパティに定数を代入する感じになります。

Bench.order = Bench.BEGIN; //実行が開始された順で表示 (デフォルト)
Bench.order = Bench.LABEL; //ラベルの辞書順で表示
Bench.order = Bench.TIME; //時間がかかったものから表示
Bench.order = Bench.COUNT; //呼び出し回数が多いものから表示

delayed list

2008.7.10

前回紹介した反復処理ライブラリは、mapやfilterなどの一部のメソッドで遅延リストを返すようにして部分的にストリームのパラダイムを取り入れていました。要素は要求に応じて計算されるまで存在しないのにも関わらず、利用者からはあたかもリスト全体が存在しているかのように見え、リスト演算でプログラムを表すことができます。

例えば、n番目の素数を見つけるプログラムは:

function isPrime(n:uint):Boolean {
  function smallestDivisor(n:uint):uint {
    return Iterable
      .range(2, Math.sqrt(n) + 1) //2から√nまでの数のうち
      .find(function(divisor:uint):Boolean { //nを割り切れる数が最小除数
        return n % divisor == 0;
      }) || n; //それがなければnが最小除数
  }
  return n == smallestDivisor(n); //最小除数が自身であれば素数
}

function primeAt(n:uint):uint {
  return Iterable
    .count(2) //2以上の整数のうち
    .filter(isPrime) //素数であるものの
    .slice(n - 1, n).toArray()[0]; //n番目
}

trace(primeAt(1000)); // => 7919

とストレートに書けます。toArrayのタイミングで評価が強制され、countからは必要な数だけ整数が供給されます。これがもし遅延リストでなく、通常の配列ならどうでしょうか?仮に「100以下の素数を求めるプログラム」という問題なら以下のようにすればいいだけです。

//符号なし整数の区間を配列で返す
function range(a:uint, b:uint):Array {
  var ary:Array = [];
  for (var i:uint = a; i <= b; ++i) ary.push(i);
  return ary;
}

range(2, 100).filter(function(e:uint, ...rest):Boolean { //引数を合わせるためにラップ
  return isPrime(e);
});

ところが、「素数のn番目を求める」プログラムを配列の演算で表すには、事前にいくつまでの整数を生成したら十分か見積もれない、正確に見積もれたとしたらそれこそが欲している答えになってしまいますから無理があります。

遅延リストを導入することでリストが利用できる機会が広がって楽しくなります。
# AS3の記事ではありますが、半ばSICPの勉強メモとなってしまいました。。

generator

2008.7.6

ActionScript3でPythonっぽい(?)外部イテレータを利用する話です。既出だったらどうしよう。。

http://www.be-interactive.org/index.php?itemid=40
http://www.be-interactive.org/?itemid=39

数年前、ActionScript神がRubyのEnumerableをASでやっていました。eachメソッドさえ実装すれば、様々な便利メソッドがミクシンで使えるようになるというものです。その後AS3になり、Array#mapとか標準で使えるようにはなったのですが、クロージャに要求する引数の数が多くてちょっと使いにくいと思います。

そこで以下のような反復処理ライブラリをやってみたくなるわけです。

  • Python的な外部イテレータで無限リストとか扱えるようにしてみよう。
  • 神に習って反復をbreakする方法を提供しよう。
  • 大クラス主義で作ってメソッドチェインを促進しよう。

more...

assertGC()

2008.7.5

ActionScript3にて特定のオブジェクトがGCされたかを確認する話です。既出だったらどうしよう。。

http://d.hatena.ne.jp/kkanda/20080406/p1

上記の記事は監視オブジェクトの生存数の変化を通知してくれるものみたいですが、一つのオブジェクトにつき一つのディクショナリを作れば特定オブジェクトのGCをフックできそうです。以下のような感じで:

GC.watch({}, function():void {trace("gc!");});

さらにGC強制発動と併用すると、参照がどこにもなくなってGCされるべきオブジェクトを表明する関数が考えられます。表明をパスすれば安心して作業が進められそうですね。使用例は以下のような感じに:

package {
  import flash.display.Sprite;
  import flash.events.MouseEvent;

  public class Main extends Sprite {
    public var o:Object;

    public function Main() {
      o = {};
      var item:GCItem = GC.watch(o);
      stage.addEventListener(MouseEvent.CLICK, function(e:MouseEvent):void {
        o = null; //ここをコメントアウトすれば表明に違反してエラー
        item.assertGC(); //ここでエラーが起きなければGCされたことになる
      });
    }
  }
}

GC.watch()の戻り値を取っておき、参照がなくなったと思われる時点でassertGCを呼んでやれば、メソッド内部で強制GCを試みて、もし回収されていなかった場合にエラーを送出します。もっとも、強制GCの手法が非公式らしいのでいつまで機能するかは不明です。

ソースは以下のような感じになります:

package {
  import flash.display.Sprite;
  import flash.events.Event;
  import flash.net.LocalConnection;

  public class GC {
    private static var sprite:Sprite = new Sprite();

    public static function start():void {
      try {
        new LocalConnection().connect("foo");
        new LocalConnection().connect("foo");
      } catch (e:*) {}
    }

    public static function watch(value:Object, callback:Function = null):GCItem {
      var item:GCItem = new GCItem(value);
      value = null;
      sprite.addEventListener(Event.ENTER_FRAME, function(e:Event):void {
        if (!item.target) {
          callback && callback();
          e.target.removeEventListener(e.type, arguments.callee);
        }
      });
      return item;
    }
  }
}
package {
  import flash.utils.Dictionary;	

  public class GCItem {
    private var dict:Dictionary;

    public function GCItem(value:Object) {
      dict = new Dictionary(true);
      dict[value] = true;
    }

    public function get target():Object {
      for (var key:Object in dict) return key;
      return null;
    }

    public function assertGC():void {
      GC.start();
      if (target) {
        throw new Error("the object is not collected: " + target);
      }
    }
  }
}

一つ問題があって、最後の参照がなくなってから最低1フレーム経ってからGC.startしないと、GCの検出がうまかいかないような雰囲気です。従って、以下の例は期待に反して、表明に違反してしまいます。

var o:Object = {};
var item:GCItem = GC.watch(o);
o = null;
item.assertGC();

一方、以下のようにすれば期待通りに行きます。

var o:Object = {};
var item:GCItem = GC.watch(o);
o = null;
stage.addEventListener(Event.ENTER_FRAME, function(e:Event):void {
  item.assertGC();
  e.target.removeEventListener(e.type, arguments.callee);
});