AndroidのNDKでNEONを使っていて、Javaのdoubleと混載した場合に、doubleの変数が破壊されたので調べてみました。まず、NEONのレジスタのd8-d15がFPUと共有されていて、このレジスタの退避ルールはcallee savedで、関数の呼びだされ側、つまりNDKのコード側で退避しなければならないようです。


I’ve only used assembly programming so far, so I will generally describe techniques under assembly terms, but everything should be equally applicable when programming with intrinsics, just with a different syntax. There is one important thing, however, which is applicable only when you program in assembly (you don’t have to worry about that when using intrinsics): make sure you save and restore d8 to d15 if you use them in your function, as the ABI specifies that these registers must be preserved by function calls. If you don’t, it may work fine for a while, until you realize with horror that the floating-point variables in the calling functions become corrupted, and all hell breaks loose. So make sure these registers are saved and restored if they are used.
引用:Introduction to NEON on iPhone


上記にはintrinsicで書かれていれば特に気にしなくても問題無い(コンパイラが自動的にケアする)と書かれていて、実際にiOSでは問題が出ないのですが、Android環境では問題が出ていて、オブジェクトファイルを逆アセンブルしてみても退避コードが生成されていないです。(GCCは4.5.3を使っています)

ちなみにVP8では自前で退避しているようです。


Regarding to your question on
#if HAVE_ARMV7
void vp8_push_neon(INT64 *store);
void vp8_pop_neon(INT64 *store);
#endif
this code is written for ARM7 NEON. For ARM NEON, d8-d15 are
callee-saved registers. vp8_push_neon() saves d8-d15 values before
going into decoding, and vp8_pop_neon() restores those saved values
after decoding is done. The prevents problem when these registers are
used in user application and their values need to be preserved before
and after the decoding. For example, if you measure time before and
after the decoding for testing performance, the beginning time needs
to be preserved, which is floating-point number and can be in d8-d15.
Therefore, d8-d15 values have to be stored to make it right.
引用:Some questions about VP8 decoder assembly functions


ということで、次のようなコードでNEONを使う関数を囲むことで、レジスタを退避してみました。

#define PUSH_NEON if(check_neon()){asm("vpush {d8-d15}");}
#define POP_NEON if(check_neon()){asm("vpop {d8-d15}");}

結果、JAVA側のdoubleの変数が破壊されなくなったので、やはりレジスタの退避はうまくいってなかったようです。

推測するに、JNIを使うとmain関数が無いため、コンパイラがレジスタ退避コードを入れる場所を上層へ押し込んでいった結果、JNIのトップを最上位と認識できず、入れられなかったのかなと考えています。iOSの場合は、スタティックライブラリなので、コンパイラが自動的に入れられた予感です。

ただ、これだとまだ問題があります。コンパイラがvpushとvpopを認識できずに最適化してしまって、スタックを破壊してしまうことがあります。より安全には、スタックではなくローカル変数にNEONレジスタの内容を退避します。

具体的に、次のようなアセンブラファイルを作ってsave_neon_reg.sで保存します。


@ save callee saved neon registers {d8-d15}

 .equ DO1STROUNDING, 0

  .global push_neon
  .type push_neon, function
  .global pop_neon
  .type pop_neon, function

  @ ARM
  @
  @ PRESERVE8

.text
.p2align 2

_push_neon:
  push_neon: @ PROC
  vst1.i64 {d8, d9, d10, d11}, [r0]!
  vst1.i64 {d12, d13, d14, d15}, [r0]!
  bx lr

  .size push_neon, .-push_neon @ ENDP

_pop_neon:
  pop_neon: @ PROC
  vld1.i64 {d8, d9, d10, d11}, [r0]!
  vld1.i64 {d12, d13, d14, d15}, [r0]!
  bx lr

  .size pop_neon, .-pop_neon @ ENDP

  .section .note.GNU-stack,"",%progbits


Android.mkにsave_neon_reg.sを追加します。


LOCAL_SRC_FILES += \
_save_neon_reg.s.neon \


NEONを使う関数を呼ぶ前後でpush_neonとpop_neonを呼びます。


typedef long long INT64;
const int NEON_REGISTER_SIZE=8;

extern void push_neon(INT64 *store);
extern void pop_neon(INT64 *store);

int is_neon=check_neon_support();

void hoge(void){
 INT64 dx_store_reg[NEON_REGISTER_SIZE];
 if(is_neon){
  push_neon(dx_store_reg);
 }
 NEONを使う処理
 if(is_neon){
  pop_neon (dx_store_reg);
 }
}


これでばっちりです。