SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

特集記事

ランタイムから迫るAndroidアプリのパフォーマンスチューニング ~ Dalvik/ART/NDKでの性能計測・比較・改善

チューニング③「NDKの導入」

 前節まではDalvik/ART上でのチューニングを行ってきましたが、本節ではさらなるチューニングの手段として「NDKの導入」を行います。

 アプリの開発中にパフォーマンスで困ったらNDKで高速化という話はよく出てきますが、具体的にどれくらいのパフォーマンス改善を望めるのかの目安がありません。そもそも、DalvikはJITによって、ARTはAOT(Ahead-Of-Time)によって、重要な部分は何もしなくてもNativeで実行されるのですから、わざわざ開発者が高速化を目的にNDKを利用する必要があるのか、という疑問もあります。

 そこで、NDKの導入により実際にどれほど高速化されるのかを、スライドパズルアプリで調べてみます。

 実装にはC言語を採用しました。C言語での実装の場合、Dalvik/ARTと違い、ヒープ領域の管理は開発者に任されます。そこで、Dalvik/ARTでのGCに変わる手段として今回は参照カウント方式を採用しました。「参照がゼロになった時点で即時解放」という単純な仕組みですが、GCで問題になっていた生存オブジェクトの探索コストがかからなくなるため、大幅な高速化が期待できます。

 NDKを導入するのはスライドパズルアプリの中核である探索エンジン部分です。UI制御や問題の入力などはボトルネックではないので、実装はJavaのままとします。

 また、アルゴリズムや処理内容での差異をなくすため、C言語で実装する探索エンジンはJava版と同一処理を実装します。実は、NDKによるC言語での実装を元々見越していたため、Java版の探索エンジンにはMap/Setなど標準コレクションを一切利用せず、代わりに2種類の軽量コレクションを独自に実装してあります。つまり、オブジェクトの確保・解放処理以外はほぼ同一の処理内容となります。

 JavaからC言語への置き換えの例として、前節でも出てきた盤面を表すJava版の Field クラスを、C言語版では Field 構造体として実装しています。両者を比較してみましょう。

public static class Field {
    // 盤面
    private int[] field;
    // equalsの呼び出し回数が多いのでhashCodeを事前に計算して保持しておく。
    // 盤面(field)のみに依存
    private int hashCode;
    // 移動の回数
    private short operationCount;
    // 移動の記録(アクティブ)、operationsと合わせることで完全な移動の履歴になる
    private byte currentOperation;
    // 移動の記録(非アクティブ)、immutable。
    // 子孫およびFieldOperationと同一オブジェクトを共有する
    private byte[] operations;
    public Field(FieldOperation fieldOperation) {
        init(fieldOperation);
    }
    public void init(FieldOperation fieldOperation) {
        this.field = fieldOperation.field;
        this.operations = fieldOperation.operations;
        this.currentOperation = fieldOperation.currentOperation;
        this.operationCount = fieldOperation.operationCount;
        this.hashCode = 0;
        for (int fieldEntry : this.field) {
            this.hashCode ^= fieldEntry;
            this.hashCode *= 13;
        }
    }
    @Override
    public int hashCode() {
        return hashCode;
    }
    @Override
    public boolean equals(Object obj) {
        if (obj == null || !(obj instanceof Field)) {
            return false;
        }
        Field another = (Field) obj;
        if (hashCode != another.hashCode
                || !Arrays.equals(field, another.field)) {
            return false;
        }
        return true;
    }
    public String getOperations(boolean reverseMode) {
        return FieldOperation.getOperations(operations, currentOperation, 
            operationCount, reverseMode);
    }
}
typedef struct {
    Uint32Array *field;
    Uint8Array *operations;
    int reference;
    int hashCode;
    uint16_t operationCount;
    uint8_t currentOperation;
} Field;

Field *Field_alloc(FieldOperation *fieldOperation) {
    Field *this = malloc(sizeof(Field));
    if (this == NULL) {
        return NULL;
    }
    this->reference = 1;
    this->field = fieldOperation->field;
    Uint32Array_retain(this->field);
    this->operations = fieldOperation->operations;
    Uint8Array_retain(this->operations);
    this->currentOperation = fieldOperation->currentOperation;
    this->operationCount = fieldOperation->operationCount;

    int i;
    this->hashCode = 0;
    for (i = 0; i < this->field->length; ++i) {
        this->hashCode ^= this->field->list[i];
        this->hashCode *= 13;
    }

    return this;
}
int Field_equals(Field *this, Field *another) {
    if (another == NULL) {
        return 0;
    }
    if (this->hashCode != another->hashCode
            || Uint32Array_equals(this->field, another->field) == 0) {
        return 0;
    }
    return 1;
}
void Field_retain(Field *this) {
    this->reference++;
}
void Field_release(Field *this) {
    if (--this->reference <= 0) {
        Uint32Array_release(this->field);
        Uint8Array_release(this->operations);
        free(this);
    }
}
void Field_getOperations(Field *this, unsigned char reverseMode,
        char *operationsString) {
    FieldOperation_getOperations(this->operations, this->currentOperation, 
        this->operationCount, reverseMode, operationsString);
}

 

 Javaではコンストラクタで自動的にヒープ領域に確保されるのに対し、C言語では Field_alloc() および Field_release() 関数の内部でmalloc/freeを利用して領域の確保・解放を行っています。また、Javaでは equals() および hashCode() メソッドの規約に従うために int hashCode() メソッドをオーバーライドしているとか、C言語の Field_getOperations() 関数と値の戻し方が異なるとかいうように細かな差はあるものの、両者は同一の処理となっています。

 では、測定結果を見てみましょう。

 

NDKの導入によるパフォーマンス改善の結果
  プール Field 処理時間(秒) 処理速度(問/分)
Dalvik OFF 標準 38,959 7.70
Dalvik ON 標準 28,529 10.52
Dalvik ON 配列化 16,740 17.92
ART OFF 標準 24,169 12.41
ART ON 標準 21,950 13.67
ART ON 配列化 18,067 16.60
NDK OFF 標準 8,784 34.15
PC(Corei5 3.4GHz) OFF 標準 179 1,675.98
PC(Corei5 3.4GHz) ON 標準 179 1,675.98
PC(Corei5 3.4GHz) ON 配列化 230 1,304.35

 

 Dalvikの初期状態から比べると4.44倍、最も高速化したプールON、Fieldオブジェクトの配列化状態と比べても1.91倍というパフォーマンスが出ました。高速化に最も寄与したのは、GCが一切不要になったことです。他にも、JIT/AOTで生成されるJava特有のオーバーヘッドを含んだネイティブコードよりも、開発者がシンプルに書いた実装のほうがAndroidの環境では実行効率が良いことが考えられます。

次のページ
チューニング 番外編「ARM向けの最適化」

この記事は参考になりましたか?

特集記事連載記事一覧

もっと読む

この記事の著者

羽山 純(ハヤマ ジュン)

株式会社ラクーン 技術戦略部 テクニカルディレクター。開発からインフラまでシステム全般を担当。株式会社ラクーン技術ブログも運営してます。

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/8333 2014/12/16 14:00

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー