Android 4.4 のARTのコンパイルしたコードを見た感想
前回(Android 4.4 のARTのブートログを見てみた - 組み込みの人。)の続きです。取り出したoatファイルをoatdumpコマンドで見てみます。
$ oatdump --oat-file=system@priv-app@Launcher2.apk@classes.dex > Launcher2.dump
Launcher2.dumpの先頭の5000行をここに貼りました。
この結果の先頭部分はこんな感じです。
MAGIC: oat 007 CHECKSUM: 0x43c6bd69 INSTRUCTION SET: Thumb2 DEX FILE COUNT: 1 EXECUTABLE OFFSET: 0x0008e000 IMAGE FILE LOCATION OAT CHECKSUM: 0x1261e085 IMAGE FILE LOCATION OAT BEGIN: 0x60a95000 IMAGE FILE LOCATION: /data/dalvik-cache/system@framework@boot.art@classes.dex (/hd1004/opt/koba/android-4.4/out/target/product/generic/data/dalvik-cache/system@framework@boot.art@classes.dex) BEGIN: 0xf6ee3000 END: 0xf7048af2 OAT DEX FILE: location: /system/priv-app/Launcher2.apk checksum: 0xde922d68 0: Lcom/android/launcher/R$styleable; (type_idx=210) (StatusVerified) 0: void com.android.launcher.R$styleable.() (dex_method_idx=849) DEX CODE: 0x0000: const/4 v5, #+3 0x0001: const/4 v4, #+2 0x0002: const/4 v3, #+1 0x0003: const/4 v2, #+0 0x0004: const/16 v0, #+8 0x0006: new-array v0, v0, int // type@585 0x0008: fill-array-data v0, +104 0x000b: sput-object v0, [I com.android.launcher.R$styleable.AppsCustomizePagedView // field@77 0x000d: const/4 v0, #+5 0x000e: new-array v0, v0, int // type@585 0x0010: fill-array-data v0, +116 ... 中略 ... 0x00d2: nop 0x00dc: nop OAT DATA: frame_size_in_bytes: 16 core_spill_mask: 0x00000000 fp_spill_mask: 0x00000000 vmap_table: (nil) (offset=0x00000000) mapping_table: (nil) (offset=0x00000000) gc_map: (nil) (offset=0x00000000) CODE: (nil) (offset=0x00000000 size=0) NO CODE! 1: void com.android.launcher.R$styleable. () (dex_method_idx=850) DEX CODE: 0x0000: invoke-direct {v0}, void java.lang.Object. () // method@3827 0x0003: return-void OAT DATA: frame_size_in_bytes: 32 core_spill_mask: 0x00008020 (r5, r15) fp_spill_mask: 0x00000000 vmap_table: 0xf6f71054 (offset=0x0008e054) v0/r5, v65535/r15 mapping_table: 0xf6f7104c (offset=0x0008e04c) gc_map: 0xf6f71059 (offset=0x0008e059) CODE: 0xf6f71005 (offset=0x0008e005 size=72)... 0xf6f71004: f8d9c010 ldr.w r12, [r9, #16] ; stack_end_ 0xf6f71008: e92d4020 push {r5, lr} 0xf6f7100c: f2ad0e18 subw lr, sp, #24 0xf6f71010: 45e6 cmp lr, r12 0xf6f71012: f0c08017 bcc.w +46 (0xf6f71044) 0xf6f71016: 46f5 mov sp, lr 0xf6f71018: 9000 str r0, [sp, #0] 0xf6f7101a: 1c0d mov r5, r1 0xf6f7101c: f2430e45 movw lr, #12357 0xf6f71020: f2c62e34 movt lr, #25140 0xf6f71024: f2427018 movw r0, #10008 0xf6f71028: f2c60000 movt r0, #24576 0xf6f7102c: 1c29 mov r1, r5 0xf6f7102e: 47f0 blx lr suspend point dex PC: 0x0000 GC map objects: v0 (r5) 0xf6f71030: 3c01 subs r4, #1 0xf6f71032: f0008003 beq.w +6 (0xf6f7103c) 0xf6f71036: b006 add sp, sp, #24 0xf6f71038: e8bd8020 pop {r5, pc} 0xf6f7103c: f8d9e25c ldr.w lr, [r9, #604] ; pTestSuspend 0xf6f71040: 47f0 blx lr suspend point dex PC: 0x0003 0xf6f71042: e7f8 b -16 (0xf6f71036) 0xf6f71044: b002 add sp, sp, #8 0xf6f71046: f8d9e274 ldr.w lr, [r9, #628] ; pThrowStackOverflow 0xf6f7104a: 47f0 blx lr suspend point dex PC: 0x0000 GC map objects: v0 (r5)
DEXコードとそれをコンパイルしたコードが表示されるはずですが、いきなり最初のメソッドのCODEのところがNO CODE!となっています。
The first 5000 lines of oatdump of Launcher2 · GitHub108行目。
このメソッドはなぜかコンパイルされていません。
2番目のメソッドはそれらしいCODEが表示されています。Thumb2のアセンブラです。
DEXコードではたった2命令で合計4バイトですが、これをコンパイルしたコードのサイズは72バイトです。DEXコード(とその元になっているJavaのバイトコード)は非常にコンパクトで、それをネイティブコードにコンパイルするとこんなにサイズが膨らんでしまいます。これは極端な例ですが、平均しても10倍くらいに膨らむと思います。
コンパイルされないメソッド
CODEのところがNO CODE!になっているところは多数あります。そのうちのいくつかはDEX CODEの部分も無いのですが、それらはabstruct method やinterfaceだと思われるので問題ありません。しかし、DEX CODEがあるのにCODEが無いとしたら、そのメソッドはインタプリタで実行するしかありません。
最初はまだ何か既知の問題があって、それに引っかかるメソッドはコンパイルしないようになっているのかな?とも思いました。
しかし、NO CODE!のメソッドをいくつか見ていくと共通点が見つかりました。
メソッド名が
class { ... static { /* この部分はコンパイルされないのでインタープリタで実行される */ } ... }
static initializer は1回しか実行されません。前述のとおり、コンパイルするとコードサイズが膨らむのですが、1回しか実行されない部分は速度の利得もあまりないので、インタープリタで実行するほうが総合的に得だという判断のようです。
ARTが正式版になったときもこのようになっているかどうかは、またそのときに確認します。アプリを書く人は心の片隅に置いておいたほうがいいかもしれません。
2013.11.17 追記
static initializer (= class initializer)はコンパイルしないように以下のところではじいていました。
http://tools.oesf.biz/android-4.4.0_r1.0/xref/art/runtime/verifier/method_verifier.cc#IsCandidateForCompilation
コンパイルされたコードの最適化の具合は?
生成されたコードを見ていくと、どうやらあまり最適化を施していないようです。
例えば、以下のメソッド
2: int com.android.internal.policy.impl.BarController.access$100(com.android.internal.policy.im pl.BarController) (dex_method_idx=677) DEX CODE: 0x0000: iget v0, v1, I com.android.internal.policy.impl.BarController.mStatusBarManagerId // field@93 0x0002: return v0 OAT DATA: frame_size_in_bytes: 32 core_spill_mask: 0x00008060 (r5, r6, r15) fp_spill_mask: 0x00000000 vmap_table: 0x623430bc (offset=0x018ae0bc) v0/r5, v1/r6, v65535/r15 mapping_table: 0x63cd35c0 (offset=0x0323e5c0) gc_map: 0x63cd35c4 (offset=0x0323e5c4) CODE: 0x63cd35a5 (offset=0x0323e5a5 size=28)... 0x63cd35a4: e92d4060 push {r5, r6, lr} 0x63cd35a8: b085 sub sp, sp, #20 0x63cd35aa: 9000 str r0, [sp, #0] 0x63cd35ac: 1c0e mov r6, r1 0x63cd35ae: b126 cbz r6, +8 (0x63cd35ba) 0x63cd35b0: 6af5 ldr r5, [r6, #44] 0x63cd35b2: 1c28 mov r0, r5 0x63cd35b4: b005 add sp, sp, #20 0x63cd35b6: e8bd8060 pop {r5, r6, pc} 0x63cd35ba: f8d9e270 ldr.w lr, [r9, #624] ; pThrowNullPointer 0x63cd35be: 47f0 blx lr suspend point dex PC: 0x0000 GC map objects: v1 (r6)
レジスタの割り付けをもっと賢くしたら、命令数を減らせると思いませんか?
r0をスタックに保存しているけど、未使用なので無駄。そもそもスタックをずらす必要もないな。
とこれを手動で最適化すると以下のように命令数を減らすことができます。
cbz r1, +2 ldr r0, [r1, #44] bx lr ldr.w lr, [r9, #624] ; pThrowNullPointer blx lr
LLVMを使っているのに、このレベルの最適化が不可能とは思えません。あえてまだ最適化をかけていないのだろうと思います。
動作の安定化が優先なので、クラッシュレポートが来たときに調査しやすいようにしているのではないでしょうか。
2013.11.17 追記
このコードを生成するのにLLVMは使用されていませんでした。
コードを生成するバックエンドには、QuickとPortableの2つの選択肢があり、PortableのときにLLVMを使用するようになっています。現在のソースコードではデフォルトはQuickでした。(Portableを有効にしてビルドするとビルドエラーになりました。)
ついでに調べたことは以下のページに書き残しました。
Android 4.4のARTのソース解析メモ - 組み込みの人。
さらなる最適化の余地
他にも最適化されていないなと思うところを挙げておくと
- 空のコンストラクタの呼び出し
最初に提示した2番目のメソッド(com.android.launcher.R$styleable.
Dalvikではdexoptコマンドでこの最適化を行っていたと思います。
- NullPointerExceptionのチェック
素直に0と比較するコードを生成していますが、0番地付近を読み書き禁止にしておけば、Segmentation FaultのsignalでNullPointerのチェックを代用することができます。
OpenJDKではそうしていたと思います。
- スタックオーバーフローのチェック
リーフメソッド以外では必ずスタックオーバーフローをチェックするコードが挿入されています。これもSegmentation Faultのsignalを上手に使えば削減できるのではないかと思います。
まとめ
このように現状のARTはAOTのしくみを入れただけというレベルで、コンパイルの最適化もJavaVMの実行環境としての最適化もまだ入っていません。正式版になったときには、このあたりは改善されている可能性が高いと思っています。今後のアップデートを楽しみにしています。