Android* 開発者向けラーニングシリーズ 7: NDK ベースのインテル® アーキテクチャー向け Android* アプリケーションの開発および移植

同カテゴリーの次の記事

Android* 開発者向けラーニングシリーズ 8: インテル® プロセッサー向け Android* OS のビルド

この記事は、インテル® デベロッパー・ゾーンに掲載されている「Intel for Android* Developers Learning Series #7: Creating and Porting NDK-based Android* Applications for Intel® Architecture」の日本語参考訳です。


Native Development Kit (NDK) ツールセットを使用して、Android* アプリケーションでネイティブコードを使用することができます。これにより、既存のコードを再利用し、低レベルのハードウェアのコーディング、ネイティブコードでのみ最適な機能あるいは利用可能な機能による差別化が可能です。

この記事は、NDK ベースのインテル® アーキテクチャー向けアプリケーション開発における基本的なプロセスを紹介し、既存の NDK ベースのアプリケーションをインテル® アーキテクチャー・ベースのデバイスへ移植する簡単な例についても説明します。

1. Native Development Kit (NDK) の概要

ネイティブ・アプリケーション開発は、パフォーマンスに問題がある場合にのみ検討すべきでしょう。ネイティブ・アプリケーション開発は、複数のアーキテクチャーおよび同一のアーキテクチャーのさまざまな世代でアプリケーションをサポートしなければならないため、多大な労力が必要です。

この記事では、Java* アプリケーションの開発環境が正しく構築されており、簡単な Java* アプリケーションを作成できることを前提としています。最初に、http://developer.android.com (英語) から NDK をダウンロードしてインストールしてください。

2. NDK に付属の “Hello, world!” アプリケーションのビルド

ここでは、GNU* コンパイラーとインテル® C++ コンパイラーの両方を使用して、NDK に付属する x86 ターゲット向けの最初のサンプル・アプリケーションをビルドします。ビルドシステムを微調整して、アプリケーション、モジュール、特定のファイルなどに応じて最適化オプションを変更する方法を示します。インテル® C++ コンパイラーは Linux* ホスト上でのみサポートされているため、ここでは Linux* 環境について説明します。

2.1. 環境の準備と検証

NDK に付属するサンプルを利用するには、ndk-build ユーティリティーと 2 つの makefile (Application.mk と Android.mk) の構造に慣れておく必要があります。

ndk-build ユーティリティーは、ビルドシステムの詳細を抽象化します。Application.mk と Android.mk には、ビルドシステムを設定する変数定義が含まれています。例えば、これらの makefile ではビルドするアプリケーションのソースファイル・リストやアプリケーションが依存する外部コンポーネントなどを指定します。

次のように、PATH に NDK の ndk-build ユーティリティーと、対応する SDK の android ユーティリティーが設定されていることを確認してください。

PATH=<NDK>:<SDK>/tools:$PATH

環境を検証する最も簡単な方法は、NDK の hello-jni サンプルをリビルドしてみることです。hello-jni をコピーして、コピー先のディレクトリーで次のコマンドを実行します。

$ ndk-build –B

x86 ABI[1] をサポートするターゲットが少なくとも 1 つ実行されていることを確認します。

$ android list targets

ターゲットが表示されない場合は、次のコマンドを実行してインストールします。

$ android sdk

2.2. GNU* コンパイラーによるビルド

前のセクションでは、セットアップを検証し、libs/armeabi にある libhello-jni.so をビルドしました。ネイティブ・アプリケーションのデフォルトのターゲットは ARM* なので、ビルドしたライブラリーは ARM* でのみ実行できます。x86 向けの開発では、アプリケーションを適切に設定する必要があります。

ファイル hello-jni/jni/Application.mk を作成し、次の定義を追加します。 

APP_ABI := x86

これは、libhello-jni.so の作成にクロスコンパイラーと x86 用のバイナリーツールを使用するよう指定します。x86 向けに libhello-jni.so をビルドしてみましょう。今回は、V=1 オプションを指定して詳細な ndk-build 出力を有効にし、make によって呼び出されるコマンドと引数を確認します。以下のコマンドを実行します:
$ ndk-build –B V=1

–B オプションは、libhello-jni.so ライブラリーを完全にリビルドします。ネイティブ・ライブラリーを作成したら、テスト APK の作成を完了させます。

NDK バージョン r8b では、x86 ターゲットの名前は android-15 です。利用可能なターゲットのリストは、次のコマンドで確認できます:
$ android list targets

この時点で、図 1 のスクリーンショットのように、デバイスまたはエミュレーター上のインストール済みアプリケーションのリストに HelloJni が表示されます。

図 1: デバイスまたはエミュレーター上のインストール済みアプリケーションのリストに HelloJni が表示される

Application.mk には、パフォーマンスの最適化に関連する重要な変数がいくつかあります。

1 つ目は APP_OPTIM です。最適化されたバイナリーを生成するには、この変数の値を release に設定すべきです。デフォルトでは、アプリケーションはデバッグ用にビルドされます。デフォルトで有効な最適化オプションを確認するには、update APP_OPTIM を実行してから、”ndk-build –B V=1” を実行します。

デフォルトの最適化オプションで満足な結果が得られない場合は、コンパイル時にオプションを追加する特別な変数があります: APP_CFLAGS APP_CPPFLAGS です。APP_CFLAGS 変数で指定されたオプションは、C と C++ の両方のファイルのコンパイル時に追加され、APP_CPPFLAGS 変数は C++ ファイルにのみ適用されます。

最適化レベルを –O3 にするには、Application.mkAPP_CFLAGS:=-O3 を追加します。

2.3. インテル® C++ コンパイラーによるビルド

インテル® C++ コンパイラーは NDK には含まれていません。ndk-build ユーティリティーとともに使用する前に、NDK に統合する必要があります。新しいツールの準備を行います。

  • 新しいツール用のディレクトリーを作成します: “mkdir –p “<NDK>/toolchains/icc-12.1/prebuilt/
  • ここでは icc-12.1 を使用していますが、任意の名前を使用できます。この名前は、ndk-buildNDK_TOOLCHAIN 引数の値として使用されます。
  • インテル® コンパイラーがインストールされているトップレベルのディレクトリー <ICC_ROOT> を、<NDK>/toolchains/icc-12.1/prebuilt/intel にコピーします。ディスク容量を節約するため、代わりに <ICC_ROOT> から <NDK>/toolchains/icc/prebuilt/intel へシンボリック・リンクを作成してもかまいません。
  • GCC* ディレクトリーから config.mk と setup.mk をコピーします:

    cp<NDK>/toolchains/x86-4.6/{setup.mk,config.mk}<NDK>/toolchains/icc-12.1
  • 新しい setup.mk TOOLCHAIN_NAME 変数を任意の値 (例えば、icc) に変更します。
  • setup.mkTOOLCHAIN_PREFIX 変数が新しいツールを参照するように変更します:

        TOOLCHAIN_PREFIX := $(TOOLCHAIN_ROOT)/prebuilt/intel/bin/
  •  TOOLCHAIN_PREFIX の値の最後は ‘/’ であることに注意してください。
  • setup.mk で GCC* x86 コンパイラーへのパスを指定します:

        export ANDROID_GNU_X86_TOOLCHAIN=$(TOOLCHAIN_ROOT)/../x86-4.6/prebuilt/linux-x86/
  • ターゲットシステムのルートへのパスを指定します。システムルートは、システム・ライブラリーとヘッダーファイルの場所を特定するのに必要です:
  • export ANDROID_SYSROOT=$(call host-path,$(SYSROOT))[1]
  • インテル® コンパイラーで使用される GCC* のバージョンを指定します:
  • TOOLCHAIN_VERSION:= 4.6
  • <NDK>/toolchains/icc /setup.mk にあるインテル® コンパイラーのコンポーネントへのパスを指定します:
    • TARGET_CC:=$(TOOLCHAIN_PREFIX)icc
    • TARGET_CXX:=$(TOOLCHAIN_PREFIX)icpc
    • TARGET_LD:=$(TOOLCHAIN_PREFIX)xild
    • TARGET_AR:=$(TOOLCHAIN_PREFIX)xiar
    • TARGET_STRIP:=$(ANDROID_GNU_X86_TOOLCHAIN)/i686-android-linux/bin/strip
    • TARGET_LIBGCC:=$(shell env ANDROID_GNU_X86_TOOLCHAIN=$(ANDROID_GNU_X86_TOOLCHAIN) $(TARGET_CC) -print-libgcc-file-name)

ndk-build -B V=1 NDK_TOOLCHAIN=icc-12.1 ” を実行します。インテル® コンパイラーにより共有ライブラリーがビルドされます。ただし、これでは、アプリケーションがインテル® コンパイラーにより生成される共有ライブラリーに依存するため動作しません。最も簡単な解決方法は、インテルのライブラリーをスタティック・リンクにすることです。<WORK_DIR>/hello-jni/jni/Android.mk に次の行を追加します。

LOCAL_LDFLAGS := -static-intel

アプリケーションをリビルドして、再度インストールします。これで、エミュレーターまたはデバイス上でアプリケーションが想定どおりに動作します。

サポートされていないオプション ‘-funwind-tables’‘-funswitch-loops’ に関する警告がいくつか出力されますが、 無視しても問題ありません。オプションの互換性については後述します。

2.4. インテル® C++ コンパイラーの共有ライブラリーのパッケージ化

アプリケーションが大きい場合、スタティック・リンクは適切とは言えません。その場合、アプリケーションにライブラリーを含める必要があります。インテル® コンパイラーでビルドされたアプリケーションは、次のライブラリーに依存します[2]

  •  libintlc.so : 最適化された文字列とメモリールーチンが含まれています。
  • libimf.so および libsvml.so : 最適化された数学関数が含まれています。

インテル® コンパイラーのライブラリーを含めるには、<WORK_DIR>/hello-jni/jni/Android.mk に次の行を追加します。 

include $(CLEAR_VARS)
LOCAL_MODULE    := libintlc
LOCAL_SRC_FILES := libintlc.so include $(PREBUILT_SHARED_LIBRARY)

libimf.solibsvml.so でも同様の設定が必要です。libintlc.so、libimf.so 、および libsvml.so ライブラリーを <WORK_DIR>/hello-jni/jni にコピーします。そして、アプリケーションをリビルドして、再度インストールします。

3. インテル® C++ コンパイラー・オプション

インテル® コンパイラーは、多くの GNU* コンパイラー・オプションをサポートしていますが、すべてではありません。-funswitch-loops のように、サポートしていないオプションに対して、インテル® コンパイラーは警告を出力します。必ず警告を確認してください。

3.1.  互換性オプション

警告となる非互換性は数多くあります。これは、どの構文が危険で、どの構文が安全かということに尽きます。一般に、インテル® コンパイラーのほうが GNU* コンパイラーよりも多くの警告を出力します。GNU* コンパイラーは 4.4.3 から 4.6 の間に警告の設定が変更されました。インテル® コンパイラーでは GNU* の警告オプションをサポートするべく取り組んでいますが、GNU* コンパイラーは進化しているため、サポートは完全ではないかもしれません。

GNU* コンパイラーでは、–W<diag name> および –Werror=<diag name> というさまざまなニーモニック名を使用しています。1 つ目のオプションは指定された警告を有効にし、2 つ目のオプションは GNU* コンパイラーに警告をエラーとして扱うように指示します。その場合、コンパイラーは出力ファイルを生成せず、診断データのみ生成します。補完オプション Wno-<diag name> は対応する警告を非表示にします。GNU* コンパイラー・オプション -fdiagnostics-show-option は、オプションを無効にするのに役立ちます: 出力される警告ごとにそれを制御するヒントが示されます。

インテル® コンパイラーはこれらのオプションの一部を認識せず、無視するか、修正します。必要に応じて、–Werror オプションを使用してすべての警告をエラーにすることができます。その場合、インテル® コンパイラーによるビルドで問題が発生することがあります。この問題を回避するには、ソースコードを修正するか、–diag-disable<id> を指定してこの警告を無効にします。<id> は警告に割り当てられた一意の番号で、 警告メッセージに含まれています。警告対象の構文が危険だと思われる場合は、ソースコードを修正したほうが良いでしょう。

Android* OS イメージ全体をインテル® コンパイラーでビルドしたところ、サポートされていないオプションと、警告と認識しなくて良いオプションが見つかりました。表 1 の説明にあるように、これらのオプションのほとんどは無視できます。いくつかのオプションでは、相当するインテル® コンパイラーのオプションを使用しました。 

GNU* コンパイラー・オプション

相当するインテル® コンパイラーのオプション

-mbionic: ターゲット C ライブラリーの実装が Bionic であることをコンパイラーに知らせます。

不要。Android* 向けのインテル® コンパイラーではデフォルトのモードです。

-mandroid: Android* ABI に応じたコード生成を有効にします。

不要。Android* 向けのインテル® コンパイラーではデフォルトのモードです。

-fno-inline-functions-called-once: インライン・ヒューリスティックを無視します。

不要。

-mpreferred-stack-boundary=2: スタックポインターを 4 バイトでアライメントします。

-falign-stack=assume-4-byte

-mstackrealign: 各プロローグでスタックを 16 バイトでアライメントします。スタックのアライメントが 4 バイトであると想定する以前のコードと、16 バイトであると想定する新しいコードの互換性のために必要です。

-falign-stack=maintain-16-byte

-mfpmath=sse : スカラー FP 演算にインテル® ストリーミング SIMD 拡張命令 (インテル® SSE) を使用します。

不要。ターゲットの命令セットが少なくともインテル® SSE2 の場合、インテル® コンパイラーは FP 演算に対してインテル® SSE 命令を生成します[3]

-funwind-tables: スタックを巻き戻すためのテーブルの生成を有効にします。

不要。インテル® コンパイラーはデフォルトでこのテーブルを生成します。

-funswitch-loops : GNU* コンパイラーのヒューリスティックを無効にし、–O2 および –O1 でループの入れ替えによる最適化を有効にします。

不要。インテル® コンパイラーのヒューリスティックを使用します。

-fconserve-stack : スタックサイズを増やす最適化を無効にします。

不要。

-fno-align-jumps : 分岐ターゲットをアライメントする最適化を無効にします。

不要。

-fno-delete-null-pointer-checks: 一部の最適化の実装に必要な仮定を排除します。

不要。

-fprefetch-loop-arrays : プリフェッチ命令の生成を有効にします。プリフェッチは、パフォーマンスの低下を引き起こす可能性があります。注意して使用してください。

不要。使用する場合は -opt-prefetch を指定します。

-fwrapv : C 規格では、オーバーフローした場合の整数演算の結果は未定義です。このオプションは、結果をラップアラウンドするように指定します。指定すると、一部の最適化を無効にする可能性があります。

不要。インテル® コンパイラーではデフォルトのモードです。

-msoft-float : ソフトウェアで浮動小数点演算を実装します。このオプションは、カーネルのビルド時に使用されます。

実装されません。カーネルのビルド時に、浮動小数点演算命令の生成は無効になります。コードに浮動小数点データに対する操作が含まれている場合、インテル® コンパイラーはエラーを生成します。Android* OS イメージのビルドでは、このエラーは発生しませんでした。

-mno-mmx、-mno-3dnow : MMX® および 3DNow* 命令の生成を無効にします。

不要。インテル® コンパイラーはこれらの命令を生成しません。

-maccumulate-outgoing-args : 事前に呼び出しの出力引数用の領域をスタック上に割り当てる最適化を有効にします。

不要。

表 1: コンパイラー・オプションの比較

インテル® コンパイラーと GNU* コンパイラーの互換性に関する詳細は、以下を参照してください。

http://software.intel.com/en-us/articles/intel-c-compiler-for-linux-compatibility-with-the-gnu-compilers/ (英語)

3.2. パフォーマンス・オプション

パフォーマンスには常にトレードオフがあります。x86 プロセッサーはマイクロアーキテクチャーが異なるため、最適なパフォーマンスを得るにはプロセッサー固有の最適化が必要です。コードのチューニングを開始する前に、アプリケーションを実行するプロセッサー (インテル® プロセッサーか AMD* プロセッサーか)、アプリケーションのターゲット (スマートフォンやタブレット) などを決定すべきです。

インテル® コンパイラーのオプションの多くは、インテル® プロセッサー向けにチューニングされています。ここでは、モバイルデバイス向けのインテル® プロセッサーであるインテル® Atom™ プロセッサーをターゲットにします。この場合、最高のパフォーマンスを得るため、コンパイル時に –xSSSE3_ATOM オプションを指定する必要があります。コードが AMD* プロセッサー・ベースのデバイスで実行される場合は、代わりに –march=atom を使用します。この場合、インテル® Atom™ プロセッサーの命令をサポートするすべてのプロセッサーでアプリケーションを実行できますが、一部の強力な最適化が無効になる可能性があります。

“Hello, world!” アプリケーションのすべてのファイルに対して –xSSSE3_ATOM を有効にするには、hello-jni/jni/Application.mk に次の行を追加します。 

APP_CFLAGS := -xSSSE3_ATOM

ターゲット・プロセッサーが決定したら、最適化レベルを調整できます。デフォルトでは、ビルドシステムはデバッグモードですべての最適化を無効にする –O0 を使用し、リリースモードで –O2 最適化レベルを使用します。–O3 はより強力な最適化が可能ですが、コードサイズが増えることがあります。コードサイズが重要な場合は、–Os を試してください。

アプリケーション全体の最適化レベルは、APP_CFLAGS –O0 –O3 を追加することで変更できます。

APP_CFLAGS := -xSSSE3_ATOM  –O3

その他の最適化オプションについては、「ベクトル化」セクションと「プロシージャー間の最適化 (IPO)」セクションで説明します。

4. ベクトル化

インテル® コンパイラーは、自動ベクトル化を含む高度な最適化をサポートしています。インテル® C/C++ コンパイラーのベクトル化は、ループアンロールを行い、一度に複数の要素を操作する SIMD 命令を生成します。手動でループアンロールを行い、SIMD 命令に対応する適切な関数呼び出しを挿入することもできますが、 フォワード・スケーリングが得られず、開発コストがかさみます。高度な命令をサポートする新しいマイクロプロセッサーがリリースされるたびに、作業をやり直さなければなりません。例えば、初期のインテル® Atom™ マイクロプロセッサーは、SIMD 命令により単精度浮動小数点を効率良く処理する一方、倍精度浮動小数点を処理するループのベクトル化では利点が得られませんでした。

インテル® コンパイラーは常に最新の世代のインテル® マイクロプロセッサーをサポートしているため、ベクトル化により開発者は特定のマイクロプロセッサーの命令セットを知る必要がなく、プログラミングが容易になります。

-vec オプションは、IA32 アーキテクチャーをサポートする (インテルおよびインテル以外の) マイクロプロセッサー向けにデフォルトの最適化レベルでベクトル化を有効にします。効率良くベクトル化するには、コードを実行するターゲット・マイクロプロセッサーを指定する必要があります。インテル® アーキテクチャー・ベースの Android* スマートフォンで最適なパフォーマンスを実現するには、–xSSSE3_ATOM オプションを指定することを推奨します。インテル® C++ コンパイラーでは、最適化レベル -O2 以上でベクトル化が有効になります。

コンパイラーは自動で多くのループをベクトル化し最適なコードを生成しますが、場合によっては、開発者からのアドバイスが必要なこともあります。効率良いベクトル化における最大の課題は、コンパイラーがデータの依存関係をできるだけ正確に予測できるようにすることです。

次の手法は、インテル® コンパイラーのベクトル化を最大限に活用するのに役立ちます。

  • ベクトル化レポートの生成と理解
  • ポインターの一義化によるパフォーマンスの向上
  • プロシージャー間の最適化 (IPO) によるパフォーマンスの向上
  • コンパイラー・プラグマ

4.1.  ベクトル化レポート

メモリーのコピー操作の実装から見てみましょう。ループの構造は、Android* ソースで一般的に使用されているものです。 

// dst により参照されるメモリー位置は src により参照される
// メモリー位置と交わらないことを仮定
void copy_int(int *dst, int *src, int num) {
    int left = num;
    if(left<=0) return;
    do {
        left--;
        *dst++ = *src++;
    } while (left > 0);
}

ベクトル化の検証には、別のプロジェクトを作成せずに hello-jni プロジェクトを再利用します。新しいファイル jni/copy_cpp.cpp に関数を追加します。このファイルを jni/Android.mk のソースファイルのリストに追加します。 

LOCAL_SRC_FILES := hello-jni.c copy_int.cpp

詳細なベクトル化レポートを有効にするには、jni/Application.mkAPP_CFLAGS 変数に –vec-report3 オプションを追加します。

APP_CFLAGS := -O3 -xSSSE3_ATOM -vec-report3

libhello-jni.so をリビルドすると、リマークがいくつか出力されます。 

jni/copy_int.cpp(6): (列 5) リマーク: ループはベクトル化されませんでした: ベクトル依存関係が存在しています。                            

jni/copy_int.cpp(9): (列 10) リマーク: ベクトル依存関係: ANTI の依存関係が src 行 9 と dst 行 9 の間に仮定されました。    

jni/copy_int.cpp(9): (列 10) リマーク: ベクトル依存関係: FLOW の依存関係が dst 行 9 と src 行 9 の間に仮定されました。

コンパイラーが利用できる情報が少なすぎるため、残念ながら自動ベクトル化に失敗しました。ベクトル化されると、 

*dst++ = *src++;

は次のように置換されます。

*dst = *src;

*(dst+1) = *(src+1);

*(dst+2) = *(src+2);

*(dst+3) = *(src+3);

dst += 4; src += 4;

そして、最初の 4 つの代入は SIMD 命令で一度に実行されます。ただし、代入の左辺でアクセスするメモリー位置に右辺でもアクセスする場合、代入の並列実行は無効です。例えば、dst+1src+2 と等しい場合、dst+2 アドレスにある最終値は正しくありません。

リマークは、コンパイラーにより仮定されるベクトル化の妨げになる依存性の種類を示します。

  • FLOW 依存性は、前のストア操作と後のロード操作が同じメモリー位置を使用しているケースです。
  • 対照的に、ANTI 依存性は、前のロード操作と後のストア操作が同じメモリー位置を使用しているケースです。
  • OUTPUT 依存性は、2 つのストア操作が同じメモリー位置を使用しているケースです。

コード中のコメントから、dst src により参照されるメモリー位置はオーバーラップしないことが分かります。この情報をコンパイラーに知らせるには、dst および src 引数に restrict 修飾子を追加するだけです。 

void copy_int(int * __restrict__ dst, int * __restrict__ src, int num)

restrict 修飾子は、1999 年に制定された C 規格 (C99) で追加されました。C99 のサポートを有効にするには、–std=c99 オプションを指定します。C++ およびその他の C 方言では、–restrict オプションで有効にすることもできます。上記のコードに restrict キーワードの同義語である __restrict__ キーワードを追加します。

再度ライブラリーをリビルドすると、ループがベクトル化されます。

jni/copy_int.cpp(6): (列 5) リマーク: ループがベクトル化されました。

この例では、最初、コンパイラーの保守的な解析によりベクトル化に失敗しました。ループがベクトル化されない原因はこのほかにもあります。

·      命令セットが原因で効率良くベクトル化できない場合は、次のようなリマークが出力されます。

    • 非ユニットストライドが使用されています
    • データ型が混在しています
    • ベクトル化には不適切な演算子です:
    • XX 行目にベクトル化できない文が含まれています
    • 条件は例外を保護します:

·      コンパイラー・ヒューリスティックによりベクトル化が妨げられると、次のような診断メッセージが出力されます。この場合、ベクトル化は可能ですが、パフォーマンスが低下することがあります。

    •  ベクトル化は可能ですが非効率です
    • トリップカウントが低すぎます
    • 内部ループではありません

·      ベクトライザーが原因の場合は、次のようなメッセージが出力されます。

    • 条件が複雑すぎます
    • インデックスが複雑すぎます:
    • サポートされていないループ構造です

ベクトライザーにより出力される情報は –vec-reportN により制御されます。詳細は、コンパイラーのドキュメントを参照してください。

4.2. プラグマ

前述のように、restrict ポインター修飾子によってデータ依存性に関する保守的な仮定を回避できます。しかし、場合によっては restrict キーワードの挿入が困難なこともあります。また、ループで多数の配列にアクセスする場合、すべてのポインターに注釈を付けるのは大変です。このようなときにベクトル化を簡単に行えるように、インテル固有の “simd” プラグマがあります。反復間に依存関係がなければ、このプラグマを使用して内部ループをベクトル化できます。

simd プラグマは、ネイティブの整数型または浮動小数点型を処理する for ループにのみ適用されます[4]

  • for ループは、ループの実行開始前に反復回数が判明している可算ループでなければなりません。
  • ループは最内ループでなければなりません。
  • ループ内のすべてのメモリー参照でフォルトが発生してはなりません (これはマスク付きの間接参照で重要です)。

プラグマを使用してベクトル化するため、前述のコードを for ループを使用して書き直します。

void copy_int(int *dst, int *src, int num) {
    #pragma simd
    for (int i = 0; i < num; i++) {
        *dst++ = *src++;
    }
}

変更したコードをリビルドすると、ループがベクトル化されます。

“pragma simd” を使用するために簡単なループの再構成を行い、Android* OS ソースに “#pragma simd” を挿入することで、Softweg ベンチマークのパフォーマンスが変更前と比べて 1.4 倍向上しました。

ここまでは、コードを良く理解している場合、パフォーマンス・チューニングを行う前にとるべきアプローチについて説明しました。コードに精通していない場合は、コンパイラーが解析の範囲を拡大してより詳細な解析を行えるように支援できます。コピー操作の例では、コンパイラーは copy_int ルーチンの引数について全く情報を持っていないため、保守的な仮定をとらざるをえません。解析で呼び出し位置が判明すると、コンパイラーはベクトル化した場合に引数が安全であることを証明しようと試みます。

解析の範囲を拡大するには、プロシージャー間の最適化 (IPO) を有効にします。その一部は、すでに単一ファイルのコンパイル時にデフォルトで有効にされています。プロシージャー間の最適化 (IPO) については 4.4 節で説明します。

4.3 自動ベクトル化の制限

–mno-sse オプションによりカーネルモードでは SIMD 命令が無効にされるため、ベクトル化により Linux* カーネルコードをスピードアップすることはできません。この制限は、カーネル開発者により意図的に設けられています。

4.4. プロシージャー間の最適化 (IPO)

複数の関数にわたって解析可能な場合、コンパイラーはさらに最適化を行うことができます。例えば、関数呼び出しの引数が定数であることが分かっている場合、コンパイラーはその関数の定数引数バージョンを作成し、 後で引数の値に関する情報に基づいてこのバージョンを最適化できます。

単一ファイルで最適化を有効にするには、–ip オプションを指定します。このオプションを指定すると、コンパイラーはシステムリンカーが処理可能な最終オブジェクト・ファイルを生成します。オブジェクト・ファイルを生成する欠点は、ほぼ全ての最適化に必要な情報が失われてしまうことです。コンパイラーは、オブジェクト・ファイルから情報を抽出しようと試みることさえしません。

この情報損失のため、単一ファイルを解析するだけでは十分ではないことがあります。その場合は、–ipo オプションを使用します。このオプションを指定すると、コンパイラーはファイルを中間表現にコンパイルし、この中間表現はリンクやアーカイブ時にインテルの特別なツール (xiarxild) をにより処理されます。

xiar はスタティック・ライブラリーを作成するため GNU* アーカイバー ar の代わりに使用し、xild は GNU* リンカー ld の代わりに使用します。これらのツールは、リンカーとアーカイバーを直接呼び出す際にのみ必要です。より適切なアプローチは、最終リンクにコンパイラー・ドライバー icc または icpc を使用することです[5]。範囲を拡大する欠点は、個別のコンパイルの利点が失われることです。ソースを変更するたびにリンクし直さなければならず、全体のリビルドが必要になります。

グローバル解析により利点が得られる高度な最適化手法は数多くあります。そのいくつかは、一部の最適化はインテル固有のものであり、–x* オプションで有効になります[6]

Android* では、共有ライブラリーに関してはやや複雑です。デフォルトでは、すべてのグローバルシンボルはプリエンプト可能です。プリエンプト可能かどうかは、例を使って説明すると分かりやすいでしょう。1 つの実行ファイルに 2 つのライブラリーをリンクする例について考えてみます。

libone.so:
int id(void) {
  return 1;
}
libtwo.so:
int id(void) {
  return 2;
}
int foo(void) {
  return id();
}

icc –fpic –shared –o <libname>.so <libname>.c と、コマンドを実行するだけでライブラリーが作成されます。必須オプションは –fpic[7]–shared[8] のみです。 

システムのダイナミック・リンカーが libtwo.so ライブラリーをロードする前に libone.so をロードすると、foo() 関数からの id() の呼び出しは libone.so ライブラリーで解決されます。

foo() 関数を最適化する際、コンパイラーは libtwo.so ライブラリーの id() に関する情報を得ることができません。例えば、id() 関数をインライン展開できません。コンパイラーが id() 関数をインライン展開すると、libone.solibtwo.so を含むシナリオが成り立たなくなります。

そのため、共有ライブラリーを記述する場合は、プリエンプト可能な関数を記述すべきです。デフォルトでは、すべてのグローバル関数および変数は、共有ライブラリーの外で可視であり、プリエンプト可能です。このデフォルトの設定は、ネイティブメソッドを実装するには不便です。その場合、Dalvik* Java* 仮想マシンで直接呼び出されるシンボルのみエクスポートする必要があります。

シンボルの可視属性は、シンボルがモジュールの外で可視かどうか、プリエンプト可能かどうかを指定します。

  •  可視属性が “default” の場合、グローバルシンボル[9] は共有ライブラリーの外で可視であり、プリエンプト可能です。
  •  可視属性が “protected” の場合、シンボルは共有ライブラリーの外で可視ですが、プリエンプト可能ではありません。
  •  可視属性が “hidden” の場合、グローバルシンボルは共有ライブラリー内でのみ可視であり、プリエンプションは禁止されます。

hello-jni アプリケーションに戻りましょう。デフォルトの可視属性を hidden にし、JVM 用にエクスポートされる関数の可視属性を protected にします。

デフォルトの可視属性を hidden に設定するには、jni/Application.mkAPP_CFLAGS-fvisibility=hidden を追加します。

APP_CFLAGS := -O3 -xSSSE3_ATOM  -vec-report3 -fvisibility=hidden -ipo

Java_com_example_hellojni_HelloJni_stringFromJNI の可視属性をオーバーライドするには、関数定義に protected 属性を追加します。

Jstring __attribute__((visibility("protected")))

  Java_com_example_hellojni_HelloJni_stringFromJNI(JNIEnv* env, jobject thiz)

リビルドして、再度インストールします。


[1] この変数は、インテル® C/C++ コンパイラー 13.0 と NDK の統合には必要ありません。

[2] インテル® C++ コンパイラー 13.0 には追加ライブラリー libirng.so があり、擬似乱数を生成する最適化された関数が含まれています。

[3] スカラー SSE 命令は、拡張精度で演算を行いません。中間の計算に拡張精度が必要な場合は、use –mp オプションを使用してください。

[4] このほかにも複数の小さな制限があります。リファレンスを確認してください。構文違反があった場合や simd プラグマによる最適化に失敗した場合、コンパイラーは警告を出力します。

[5] 検証では、NDK の設定時に、TARGET_AR が xiar を、TARGET_LD が xild を参照するようにしました。

[6] 例えば、インテル® Atom™ プロセッサーをターゲットにする場合は –xSSSE3_ATOM を指定します。

[7] –fpic オプションは、任意のアドレスでロードできる方法でコードをコンパイルするように指定します。PIC はメモリー位置に依存しないコード (Position Independent Code) の略です。

[8] –shared オプションは、リンクされるモジュールが共有ライブラリーであることを示します。

[9] グローバルシンボルは、ほかのコンパイル単位から見ることができるシンボルです。グローバル関数および変数は、共有ライブラリーを構成するすべてのオブジェクト・ファイルからアクセスできます。可視属性は、異なる共有ライブラリーの関数と変数の関係を示します。 


[1] アプリケーション・バイナリー・インターフェイス。Android* は i386 アーキテクチャー向けの System V ABI を採用しています: http://www.sco.com/developers/devspecs/abi386-4.pdf (英語)。

[2]適切に構成されたパッケージの署名を必要とするため、検証では ant release コマンドを使用しませんでした。

コンパイラーの最適化に関する詳細は、最適化に関する注意事項を参照してください。

関連記事