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!のメソッドをいくつか見ていくと共通点が見つかりました。

メソッド名がのものはコンパイルされないようです。これはstatic initializerでJavaのソース上では以下のところです。

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.())では、void java.lang.Object.()を呼んでいますが、これは中身が空であることがわかっています。そのため、この呼び出しはnopに置き換えることが可能です。そうするとこのメソッド自体が、nop; void returnになるので、このメソッドを削除し、このメソッドの呼び出しをnopに置き換えることが可能です。
Dalvikではdexoptコマンドでこの最適化を行っていたと思います。

素直に0と比較するコードを生成していますが、0番地付近を読み書き禁止にしておけば、Segmentation FaultのsignalでNullPointerのチェックを代用することができます。
OpenJDKではそうしていたと思います。

  • スタックオーバーフローのチェック

リーフメソッド以外では必ずスタックオーバーフローをチェックするコードが挿入されています。これもSegmentation Faultのsignalを上手に使えば削減できるのではないかと思います。

まとめ

このように現状のARTはAOTのしくみを入れただけというレベルで、コンパイルの最適化もJavaVMの実行環境としての最適化もまだ入っていません。正式版になったときには、このあたりは改善されている可能性が高いと思っています。今後のアップデートを楽しみにしています。