2011/11/27

macosxでANE

AIR Native Extensionを使ってAIRが標準でサポートしない機能を使ったモバイルアプリを作ることができるようになりました。

では、デスクトップアプリはどうでしょうか?デスクトップアプリの場合は、標準でサポートされていない機能を使いたいというよりは、既にあるコードを流用したり、処理の重いルーチンをネイティブコードに置き換えて高速化したいという要望の方が強そうです。

■サンプルのビルド
まぁ、何はともあれビルドにはまた手を焼くだろうから、簡単なサンプルから始めてみるしかありません。ということで、Adobeが公開しているサンプルを試してみました。

このサンプルがなかなかよく出来てて、順を追って各フォルダーにあるgo.shを使ってビルドして行けば、サンプルのAIRアプリが出来るようになっているのですが、恐らくネイティブコードをビルドするところで、躓くことになると思います。

02 - create platform extension/mac/TestNativeExtension以下にxcodeのプロジェクトファイルがありますので、ダブルクリックしてxcodeを起動してみましょう。

多くの人は以下のようにAdobe AIR.frameworkのところが赤くなっていることでしょう。これはこのframeworkに設定されているパスが正しくないからです。


なので、コンテクストメニューから"情報を見る"を選択して、正しいパスを設定しましょう。


次に"プロジェクト"メニューから"アクティブアーキテクチャーを設定"で、i386が選択されていない場合は、これに設定してください。ANEはx86_64だとうまく動かないようです。


後は、オリジナルのソースだと"Adobe AIR/Adobe AIR.h"という謎のインクルードファイルを指定しているのですが、これを"FlashRuntimeExtensions.h"に置き換えて、ソースと同じ場所にコピーしておきます。

[TestNativeExtension.cppファイル内]
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
//#include <Adobe AIR/Adobe AIR.h>
#include "FlashRuntimeExtensions.h"
これで、xcode上でビルドしても問題が出なければ、次からはコマンドライン上でgo.shを使ってビルドしてもきっとうまく行くと思います。

さて、ようやく本題に入れます。

■サンプルを実行
とりあえず、サンプルを実行してみましょう。画面上部のラジオボタンで、ASのコードを使うかネイティブコードを使うか選択できるようになっているのですが、注目してもらいたいのは、画面下部にあるソートの機能です。

試しに、1万件くらいの配列を指定してASとネイティブコードの実行時間を比較して見ると、あれれ、ASの方が速いですね?


10万件にしても、100万件にしても同じです。やはりASの方が速いですね。以下は私の環境で実行した時の値です。
件数 ASコード Nativeコード
10000 0.003sec 0.005sec
100000 0.029sec 0.045sec
1000000 0.352sec 0.448sec

何ででしょうかね?と悩むより前に、ソースコード上のコメントにも「ネイティブコードはCの配列を作るオーバーヘッドがあるのでソートはASより遅いよ」と書いてあります(笑

ってことで、TestNativeExtension.cppのFREObject _Advanced_timeLongOp関数内で、該当のデータをやり取りしている部分を見てみると、
// Copy input array to sort internally
for ( uint32_t i = 0; i < arrayLength; i++ ) {
    result = FREGetArrayElementAt( argv[0], i, &obj );
    if ( result != FRE_OK ) {
        return (FREObject)NULL;
    }

    result = FREGetObjectAsInt32( obj, &largeArray[ i ]);
    if ( result != FRE_OK ) {
        return (FREObject)NULL;
    }
}
Array配列の要素ひとつひとつに対して、Int32型のオブジェクトを生成して確保したメモリー領域に格納しています。 

以下は、ソートした後のデータを元のArray配列に戻すコードですが、基本的にさっきと逆のことをしてます。
// Copy sorted result back
for ( uint32_t i = 0; i < arrayLength; i++ ) {
    result = FRENewObjectFromInt32( largeArray[ i ], &obj );
    if ( result != FRE_OK ) {
        return (FREObject)NULL;
    }

    result = FRESetArrayElementAt( argv[0], i, obj );
    if ( result != FRE_OK ) {
        return (FREObject)NULL;
    }   
}
なるほど、確かにこれはなかなかオーバーヘッドが大きそうなコードです。

このように、異なるプログラミングモデルを用いる場合に(Alchemyなんかもそうなのですが)、レイヤー間を跨ぐデータのやり取りのオーバーヘッドが大きくて、実際は速くならないというようなことがしばしば起こるわけです。

せっかく速くする為にネイティブコードを使いたいのに、返って遅くなるのでは、ANEって全然使えねーじゃん!となるところですが、今回の場合はデータ構造を少し工夫すればオーバーヘッドを軽減できそうです。

具体的には、Arrayでちまちまデータを渡すのでは無くて、ByteArrayにソートするデータをつっこんで、データ列をネイティブコード側で直接ソートできるようにデータを準備します。

まず、AS側のコードですが、
// データを格納するByteArrayを宣言
var byteData:ByteArray = new ByteArray();

// ソートするデータを格納するコード
for ( var i:int = 0 ; i < arraySz ; i++ ) {
    byteData.writeByte(i & 0xff);
    byteData.writeByte((i >> 8) & 0xff);
    byteData.writeByte((i >> 16) & 0xff);
    byteData.writeByte((i >> 24) & 0xff);
}
byteData.position = 0;

// ソート後のデータを取得するコード
var data:int = ((byteData.readByte() & 0xff)) | ((byteData.readByte() & 0xff) << 8) | ((byteData.readByte() & 0xff) << 16) | ((byteData.readByte() & 0xff) << 24);
ネイティブコード側の手間を省く為に、データをリトルエンディアンに並べておくのがコツです。ま、本来はエンディアンに依存しないコードにすべきでしょうけど。

AVM2もリトルエンディアンだと思ったのですが、ByteArray.write/readInt()ではうまくソート出来ませんでした。何か勘違いしてるかな?

では次にネイティブコード側です。
FREObject _Advanced_timeLongOp(FREContext ctx, void* functionData, uint32_t argc, FREObject argv[]) {
    // Byte列を格納する変数
    FREByteArray bytedata; 

    // ByteArrayクラスオブジェクトのバイトを取得
    FREResult result = FREAcquireByteArray(argv[0], &bytedata);
    if ( result != FRE_OK) {
        return (FREObject)NULL;
    }

    qsort(&bytedata.bytes[0], bytedata.length/4, sizeof(int32_t), cmpFcn);

    // ByteArrayクラスオブジェクトを解放する
    FREReleaseByteArray(argv[0]);

    return (FREObject)NULL;
}
え、こんだけ?えぇ、こんだけです。

qsortで直接ソートできる形式でデータ列を格納してあるので、余計なメモリー確保もデータコピーも不要になりました。これらのオーバーヘッドを軽減したことにより、ソートサンプルは以下のようにネイテイブコード側の方がASに比べてかなり速く処理できるようになりました。
件数 ASコード Nativeコード
最適化前
Nativeコード
最適化後
10000 0.003sec 0.005sec 0.001sec
100000 0.029sec 0.045sec 0.009sec
1000000 0.352sec 0.448sec 0.079sec

■あとがき
まぁ、今回のように極端に最適化できる例はあまり無いかも知れませんが、基本的にデータのやり取りのオーバーヘッドは大きいので、データ量が多い時は、ByteArrayにデータをつっこんで受け渡しするってのが、常套手段になるような気がします。

[Updated]
ソースコードのライセンスがよくわからないので、patchファイルだけane-labにアップして置きました。

0 件のコメント:

コメントを投稿