GC判定再び

2008.12.20

GCされたかテストするクラスの改良版。

最後の参照を消すとき、

_instanceVar = null; //これでいつか解放されるはずだが・・

と書いているところを、

//常に戻り値のnullが代入されるが、同時に漏れを確認
_instanceVar = GC.shouldBeGarbage(_instanceVar); // 以降、ゴミとして回収されるべき!

と書くことにします[*注]。これで参照の消し忘れがあれば、直後にエラーになってくれます。多分。

ちなみにFlexでは子SWFがアンロードされると、以下のように表示されるのでこれの出番は少ないかもしれません。

[SWF のアンロード]Users:masuda:Documents:Flex Builder 3:Test:bin-debug:child.swf

以下にいくつかサンプルを挙げます。
ドキュメントクラスのコンストラクタに書かれているコードだと思って下さい。

ローカル変数はGCされる:

var o:DisplayObject = new Sprite();
GC.shouldBeGarbage(o); //=> GC succeeded: 4489216 => 4771840

インスタンス変数に退避したため延命され、GCされない:

var o:DisplayObject = new Sprite();
_instanceVar = o;
GC.shouldBeGarbage(o); //=> Error

表示リストに残っているため、GCされない:

_instanceVar = new Sprite();
addChild(_instanceVar);
//removeChild(_instanceVar); //この行が必要だった
_instanceVar = GC.shouldBeGarbage(_instanceVar); //=> Error

ローカル変数はクロージャに延命され、クロージャはstageに延命されるのでGCされない:

var o:DisplayObject = new Sprite();
addChild(o);
removeChild(o);
GC.shouldBeGarbage(o); //=> Error
//o = null; //この行が必要だった
stage.addEventListener(
  Event.ENTER_FRAME,
  function(evt:Event):void {/*どの変数も参照してなくても害*/}
);

クロージャとメモリリークについての親切な解説はこちら:
http://www.imajuk.com/blog/archives/2008/04/post_3.html

以下、ソースコードです:

package {
  import flash.display.Sprite;
  import flash.events.Event;
  import flash.net.LocalConnection;
  import flash.system.System;
  import flash.utils.Dictionary;

  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 shouldBeGarbage(value:Object):* {
      try {
        //テストは1フレ後だが、この時点のスタックトレースがほしい
        throw new Error('Probably, it is not garbage:' + value);
      } catch (e:Error) {
        var dict:Dictionary = new Dictionary(true);
        dict[value] = true;
        value = null;

        var before:uint = System.totalMemory;
        GC.start();

        var err:Error = e;
        sprite.addEventListener(Event.ENTER_FRAME, function(evt:Event):void {
          evt.target.removeEventListener(evt.type, arguments.callee);
          for (var key:Object in dict) throw err;
          trace('GC succeeded:', before, '=>', System.totalMemory);
        });
      }
      return null;
    }
  }
}

[*注] 以下の例は、全ての参照を消すのに先だってshouldBeGarbageを行うためGCが成功しそうにありませんが、実際のところ、このように書いてもテストをパスします。

GC.shouldBeGarbage(_instanceVar);
_instanceVar = null;

恐らく、LocalConnectionによる強制GCの発動は即座に行われずに、一度Flash Playerに制御を戻した後行われているのだと想像しています・・。そうなると、shouldBeGarbage(x)の意味合いとしては、「現在実行中のコードを抜けた後には、xはゴミとなり回収されるべきだ」といったところでしょうか。

las3r (2)

2008.12.5

las3r、Clojure(むしろLisp)がよく分からないまま手探りで弄っています。便利だと思ったのが、アリティによる多重定義と、暗黙のdestructuring-bind(アンパック代入みたいなもん?)。

以下は0〜2個の引数を取るMath.random:

(defn rand
  ([] ((. Math random)))
  ([n] (* n (rand)))
  ([a b]
    (if (< a b)
      (+ a (rand (- b a)))
      (+ b (rand (- a b))))))

(rand -1 1) ;=> -0.689156367443502

要は引数のパターンに応じた振り分けと同時に変数束縛されるようですが、これが再帰関数の場合には特に便利で、例えばリストをペアに区切ったリストにして返す手続きは:

(defn pairs [l]
  (apply
    (fn
      ([x] (list (list x)))
      ([x y] (list (list x y)))
      ([x y & z] (cons (list x y) (pairs z))))
  l))

(pairs '(1 2 3 4 5)) ;=> ((1 2) (3 4) (5))

ところで、las3rでは{:x 200 :y 400 :time 0.5}と書くと内部的にはMapというオブジェクトになってしまうようなので、Tweener.addTweenの第二引数に渡せません。MapからObjectへ変換するそれらしき関数も見つからなかったので、試しにObjectリテラルに近い書き方ができるマクロを書いてみると:

(defmacro obj [& elts]
  (let [o (gensym)]
    `(let [~o (new Object)]
      ;;fnやletのパラメータはdestructuring-bindされる雰囲気。
      ~@(map (fn [[var exp]] `(set! (. ~o ~var) ~exp)) (pairs elts))
    ~o)))

(macroexpand-1 '(obj x 200 y 400 time 0.5))
;=>
;(las3r/let [G__1378 (new Object)]
;  (set! (. G__1378 x) 200)
;  (set! (. G__1378 y) 400)
;  (set! (. G__1378 time) 0.5)
;  G__1378)

(obj x 200 y 400 time 0.5) ;=> [object Object]

これで以下のようにだらだらと書かなくて済みます。

(. Tweener
  (addTween
    movie-clip
    (let o (new Object)
      (set! (. o x) 200)
      (set! (. o y) 400)
      (set! (. o time) 0.5)
      o)))

クリックすると画面上をランダムに移動する四角形はこんな雰囲気:

(import '(flash.display Sprite Graphics)
        '(caurina.transitions Tweener))

(def s ((. *stage* addChild) (new Sprite)))

(doto (. s graphics)
  (beginFill 0x0000ff 1)
  (drawRect 0 0 100 100)
  (endFill))

(. s
  (addEventListener "click"
    (fn []
      (. Tweener (addTween
        s
        (obj x (rand (. *stage* width))
             y (rand (. *stage* height))
             time 0.5))))))

最後に衝撃のfib(25)の時間比較:

  • ABC
    => 88 msec
  • AS3
    => 125 msec
  • las3r
    => 5465 msec
  • SICP4.1.7をASに移植したもの(比較用)
    => 11255 msec

なるほど・・・。
まだきっと伸びしろはありますよね!
速くなるといいなぁ。。

なお、計測の対象は以下の関数:

AS3:

private function fib(n:uint):uint {
  return (n < 2) ? n : fib(n - 1) + fib(n - 2);
}

ABC(hxasmを使用):

var ctx:Context = new Context();
ctx.beginClass("Main");
var tint:Index = ctx.type("int");
var m:* = ctx.beginMethod("fib", [tint], tint);
m.maxStack = 3;
ctx.ops([
  OpCode.OReg(1),
  OpCode.OSmallInt(1),
]);
var j:Function = ctx.jump(JumpStyle.JGt);
ctx.ops([
  OpCode.OReg(1),
  OpCode.ORet,
]);
j();
ctx.ops([
  OpCode.ODecrIReg(1),
  OpCode.OThis,
  OpCode.OReg(1),
  OpCode.OCallProperty(ctx.property("fib"),1),
  OpCode.ODecrIReg(1),
  OpCode.OThis,
  OpCode.OReg(1),
  OpCode.OCallProperty(ctx.property("fib"),1),
  OpCode.OOp(Operation.OpAdd),
  OpCode.ORet,
]);
ctx.finalize();

var o:Output = new Output();
Writer.write(o, ctx);
var swf:ByteArray = o.getBytes();

var loader:Loader = new Loader();
loader.contentLoaderInfo.addEventListener(Event.COMPLETE, function(evt:Event):void {
  var klass:Class = ApplicationDomain.currentDomain.getDefinition('Main') as Class;
  callback(new klass().fib); //この引数がコンパイル済みfib
});
loader.loadBytes(swf, new LoaderContext(false, ApplicationDomain.currentDomain));

las3r:

var rt:RT = new RT(stage, new OutputStream(trace), new OutputStream(trace));
rt.loadStdLib();
rt.evalStr(
  '(defn fib [n] (if (< n 2) n (+ (fib (- n 1)) (fib (- n 2))))) fib',
  callback //fnを評価するとコンパイル済みfibがcallbackに渡る
);

SICP4.1.7をASに移植したもの:

var fib:Function =
  eval("(define (fib n) (if (< n 2) n (+ (fib (- n 1)) (fib (- n 2))))) fib");

/*evalの定義は長いので略*/

las3r

2008.12.2

clojureというJVM向けLisp方言があるらしいのですが、それのAVM2向けコンパイラがAS3で書かれています。
http://github.com/aemoncannon/las3r/wikis

構文的な違いが結構あり戸惑ったのですが、こちらを見ればだいたいつかめると思います。

Schemeじゃないから当然かもしれませんし、ABC的に実装に難があるからかも分かりませんが、残念ながら:

  • 末尾最適化なし (末尾再帰にはrecurを使えばスタックを消費しない)
  • 継続なし

しかし、それを差し引いてもASにない以下の特徴を楽しむことができます:

  • マクロが使える
  • 動的コンパイル

しかも、Emacs向けにlas3r-modeが用意されていて、起動中のSWFに対し修正を動的に反映できます。
以下手順:

  1. etc/las3r-mode.elをEmacsのロードパスへ配置
  2. もしなければ、以下もDLしてロードパスへ配置
    http-get.el, http-cookies.el, http-post.el
  3. .emacsに以下を追記
    (autoload 'las3r-mode "las3r-mode" "Major mode for las3r." t)
    (add-to-list 'auto-mode-alist '("\\.lsr$" . las3r-mode))
  4. src/lsr/boot.lsrの最終行、;;(connect-to-eval-pipe)をコメントインしておく
  5. etc/eval_pipe.rbを起動しておく
  6. 対象のSWFを起動しておく
  7. Emacsにて*.lsrの編集中にF5 (すると、バッファ全体がevalされる)

この手の試みがじわりじわりと増えてきているような気がしますね・・。
今後が楽しみです。

関連メモ:
http://hg.mozilla.org/tamarin-central/file/d92e466aed84/esc/src/
元?

http://eval.hurlant.com/
上記のAS3版だと思う。las3rはこれの一部を使っている。

http://ariyan-harhid.cocolog-nifty.com/blog/2008/10/tamarin-esc-c-2.html
上記の改良版。実行時エラー情報を詳しく教えてくれたりetc

http://happyabc.org/
OCaml製のSchemeコンパイラ

http://www.bitbucket.org/SumiTomohiko/actionpython/overview/
これは・・?

http://haxe.org/com/libs/hxasm
haXeのABCアセンブラ

http://www.sephiroth.it/weblog/archives/2008/07/expressions_evaluation_at_almost_nati.php
上記のAS3版を基礎としたシンプルな電卓デモ

http://www.anotherbigidea.com/javaswf/avm2/AVM2Instructions.html
見やすい

http://code.google.com/p/as3c/
インラインアセンブラ