Androidとリフレクションについて

Android上でのプログラミングではリフレクションは忌避すべきと言われている。

PCに比べるとリソースが制限されているスマートフォンではこれは正しい。しかしNexus oneやXperia等、周波数1Ghzのプロセッサと500MB以上のRAMを搭載した実機を使ってきた感覚だと、性能をシビアに追求するようなアプリケーション(主にゲームやリアルタイムシミュレーション)以外であれば、無駄に繰り返さない、結果を上手くキャッシュする等、使い方を注意すれば別に避けるものではないと考えている。(感覚としては10年前のIntel Pentium3が搭載されたPC上でJava2(JavaSE1.3〜1.4)を動作させている状態に近いと思う。)

なぜこんなことを書くかというと、今更Javaでリフレクションを否定されると何もできないからだ。自分が今書いているライブラリィ、フレームワークではリフレクションを使いたい(というか既に使っているし)

そこでAndroid 2.2のリフレクションによるメソッド実行の性能はどうなのかちょっとだけ調べてみることにした。


リフレクションの性能を調べるとには同様の処理をリフレクションの有無で比較すれば良い訳だが、はてなで同様にリフレクションの性能を試験されている方がいらっしゃったので、これを利用させて頂くことにした。 (unageanu氏にはテストコードの利用とその公開を快諾して頂きまして感謝します。)

[Java] リフレクションはやっぱり遅いのか? - うなの日記

ここで書かれているコードはテストするクラスの操作をタスクというインタフェースで定義しておき、それを統合、回数を指定してまとめで実行できるものだ。また内部では同じテストを10回実行しており環境によるサンプルのばらつきがなるべく出ないように工夫されている。

これをAndroidの開発環境上に移して動作を確認し、全く同じテストコードをAndroidエミュレータ、実機のNexus one、そしてPC上で動作しているJava SE6で実行しその時間の差を比較してみることにする。
テストコードの解説はうなの日記を見て頂くとして、メソッドの実行時間の差を計測する以外にAndroidに関してはちょっと気になることがあったので、フィールドに関しても同様に「通常」「リフレクション(検索時間含まず)」「リフレクション(検索時間含む)」のテストコードを追加してその時間を計測した。※

  • 実行結果 (各テストは10万回繰返しているが、内部では更に10回実行された平均値がサンプリングされる)
[実行環境]
・Nexus one
  CPU    Qualcomm® QSD8250 1GHz
  MEM    512MB
  OS     Android 2.2 build FRF91 
 
・JavaSE6/Emulator
  CPU    Intel® Xeon X5450 3.0Ghz (×2)
  MEM    4GB
  OS     Windows® Vista with service pack 2/Android SDK 2.2 FRF91
  Java   JavaSE1.6 build 1.6.0_20-b02

[テスト結果]
タスク Emulator Nexus one JavaSE6
メソッド呼び出し 118 msec 36 msec 1 msec未満
フィールドアクセス 87 msec 31 msec 1 msec未満
リフレクション-メソッド(検索時間を含まず) 2358 msec 505 msec 43 msec
リフレクション-メソッド(検索時間を含む ) 7532 msec 1997 msec 193 msec
リフレクション-フィールド(検索時間を含まず) 686 msec 163 msec 67 msec
リフレクション-フィールド(検索時間を含む ) 3065 msec 662 msec 202 msec
結果は見ての通りだが非常に興味深い。 JavaVMが速いのは当たり前。むしろ10年前のPCと同等かそれ以下のハードウェア、アプリケーションに割けるヒープも16MBでしかないNexus oneは非常に健闘しているように見える。というのも、リフレクションにおけるフィールドアクセスは3Ghzのプロセッサを二つ搭載するPC上で動作しているJava SE6と比べて周波数比とほぼ同じ3倍程度の時間しかかかっていないのだ。これはちょっと凄くないか? どうやらAndroid2.2はリフレクションを介した場合にはメソッドを実行するよりもフィールドにアクセスするほうが遙かに効率が良いようだ。エミュレータも実機で傾向が変わらないことから、ハードウェアに依るものではないと考えられる。Javaはフィールドとメソッドで殆どが差がないことからDalvikのデータ構造に秘密がありそうな気がする。 なお、メソッドにしろフィールドにしろ検索時間の影響は大きいが、この部分の分析はまた別な機会にやりたい。 結果としてGoogle Nexus one程度の今後主流になっていくレベルのスペックであれば、使い方にさえ注意すればリフレクションを無理に避ける必要は既に無い(特にフィールドアクセスに関しては)と思うがいかがだろう。 #Android 2.2ではJITが導入されているが、今回はその有無による性能差を計測することができなかった。(エミュレータJITオフだと思うが、実機と比較はできない)実機でJITを簡単に切替える方法が解れば追試したい所だ ※うなの日記で紹介されていた一連のテストは今回のように計測用のタスクを簡単に追加できるので、非常に使いやすい。 追加したテストコードはメソッドアクセス(setName)を単純にフィールドアクセス(name)に変更しただけ。
  • ReflectionFieldTask
    static class ReflectionFieldTask implements Task {
        private Kitten mii = new Kitten();
        private Field f;

        ReflectionFieldTask() {
            try {
                f = mii.getClass().getDeclaredField("name");
            } catch ( SecurityException e ) {
                e.printStackTrace();
            } catch ( NoSuchFieldException e ) {
                e.printStackTrace();
            }
        }
        public void execute () throws Throwable {
            f.set( mii, "mii" );
        }
        public String getName () {
            return "リフレクション-フィールド(検索時間を含まず)";
        }
    }
メソッドでのベンチマークと同様にフィールドの検索時間を含んだタスクも書いているが、変わらないので省略する。