拡張インラインアセンブラの勉強 2
前回、破壊レジスタを指定して、どういうコードが生成されるか実験してみた。
破壊レジスタに "memory"
も指定できるが、この使い方がさらにわかりにくい。
そこで簡単な実験をしてみた。
int hoge()
{
int a = 10;
asm("mov r3, #100; str r3, [%0]" : : "r"(&a) : "r3");
return a;
}
というコードを最適化オプションなしで、コンパイルしてみる。
<hoge>:
push {fp} ; (str fp, [sp, #-4]!)
add fp, sp, #0
sub sp, sp, #12
mov r3, #10
str r3, [fp, #-8]
sub r2, fp, #8
mov r3, #100 ; 0x64
str r3, [r2]
ldr r3, [fp, #-8]
mov r0, r3
add sp, fp, #0
pop {fp} ; (ldr fp, [sp], #4)
bx lr
となった。
毎回、変数の値をメモリまで取りに行くので、これは 100
を返す。
一方、 -O2
オプションをつけてコンパイルしてみると
<hoge>:
sub sp, sp, #8
add r2, sp, #4
mov r3, #100 ; 0x64
str r3, [r2]
mov r0, #10
add sp, sp, #8
bx lr
となった。
これは 10
を返す。
インラインアセンブラの中で 変数 a
が書き変わっているのに気づかないので、即値 10
を返すように最適化するわけだ。
最適化オプションをつけるかつけないかで、関数の結果が変わるのはもちろん困る。
そこで、インラインアセンブラの中でメモリの中身が書き変わっていますよ、
とコンパイラに教えてあげるために、破壊リストに "memory"
を追加する。
int hoge()
{
int a = 10;
asm("mov r3, #100; str r3, [%0]" : : "r"(&a) : "r3", "memory");
return a;
}
をコンパイルすると
<hoge>:
sub sp, sp, #8
mov r3, #10
add r2, sp, #8
str r3, [r2, #-4]!
mov r3, #100 ; 0x64
str r3, [r2]
ldr r0, [sp, #4]
add sp, sp, #8
bx lr
となった。
return
の前にちゃんと変数 a
をメモリから取り直してくれるので、 100
が返る。
Linux Kernel にはメモリバリアを挿入するための barrier()
という関数がある。
定義は include/linux/compiler-gcc.h
にあって、
/* Optimization barrier */
/* The "volatile" is due to gcc bugs */
#define barrier() __asm__ __volatile__("": : :"memory")
となっている。
なんでこれがメモリバリアの役割の果たすのか、ここまで読んできたらほぼ明らかだろう。
-O2
オプションをつけてコンパイルすると
void hoge(int *a, int *b)
{
*a = 10;
*b = *a;
}
は
<hoge>:
mov r3, #10
str r3, [r0]
str r3, [r1]
bx lr となる。
最適化によって *a
からのリードが欠落している。
一方、
void hoge(int *a, int *b)
{
*a = 10;
__asm__ __volatile__("" : : : "memory");
*b = *a;
}
は
<hoge>:
mov r2, #10
str r2, [r0]
ldr r3, [r0]
str r3, [r1]
bx lr
となって、コード通り、 *a
から *b
へコピーしている。
コンパイラはメモリバリアを越えた最適化を行わなくなるが、 ハードウェアによる実行時並べ替え(Out of Order 実行)が行われる可能性はあります。
実行時のメモリアクセスの最適化を許すかどうかは ARM の場合はページテーブルの属性で指定されます。
blog comments powered by Disqus