Snapdragon 845 の CPU Kryo 385 は ARMv8.2 対応の Cortex-A75 がベースとなっています。前回新しく追加された半精度浮動小数点演算命令を使用してみましたが、他にもいろいろと生成コードに違いがあることがわかりました。そのひとつが atomic 命令です。実際に生成されたコードを比べてみました。
● Atomic 演算
まずは fetch_add() の場合。下記コードのコンパイル結果です。
std::atomic val( 0 );
val.fetch_add( 10, std::memory_order::memory_order_seq_cst );
x86/x64 では lock prefix 付きの xadd が使われています。
; x64
lock xaddl %esi, 8%(rsp)
ARMv7-A の場合 LL/SC 型なので複数の命令に分解されます。ldrex, strex が LL/SC です。途中で同一メモリにアクセスがあると Atomic 操作は失敗するので、失敗していたら loop からやり直します。またメモリアクセスの順序付けが必要な場合はメモリバリア命令 dmb が挿入されます。
; armv7a
loop:
ldrex r0, [r4]
add r2, r0, #10
strex r2, r0, [r4]
cmp r2, #0
bne loop
dmb ish
ARMv8.0 (AArch64) も同様ですが、命令体型はほぼ別ものです。またメモリバリアが命令に組み込まれているので命令数は減っています。ldaxr は ldxr(LL) + acquire に相当し、同じように stlxr は stxr(SC) + release を意味しています。これだけで memory_order_acq_rel な Atomic 操作になります。
; armv8.0a
loop:
ldaxr w9, [x8]
add w1, w9, #10
stlxr w9, w1, [x8]
cbnz w9, loop
ARMv8.1 (AArch64) では命令が拡張されており x64 同様 1命令で済むようになっています。ldaddal の最後の al が acq_rel を意味しており、他に relaxed, acquire, release があるので 4通りです。さらにサイズも 8, 16, 32, 64bit から選べるので ldadd だけでも 16 種類あることになります。
; armv8.1a
ldaddal w23, w8, [x22]
ARMv8.1 は add 以外の atomic 演算にも対応しています。
例えば x64 の場合 xadd 以外の演算命令がないので fetch_xor( 7 ) は load + xor + CAS に展開されています。
; x64
loop:
movl 8(%rsp), %ecx
movl %ecx, %edx
xorl $7, %edx
movl %ecx, %eax
lock cmpxchgl %edx, 8(%rsp)
jne loop
ARMv8.1 ではこれも 1命令です。他にも and, or 相当の ldclr/ldset があります。
; armv8.1a
ldeoral w8, w1, [x19]
● compare_exchange (CAS) の weak/strong の違い
ARMv7/ARMv8.0 では compare_exchange_weak() と compare_exchange_strong() で違いがあります。strong は LL/SC の失敗時に再実行しますが weak はそのまま抜けます。SpinLock など CAS の戻り値によって繰り返し呼び出す用途では、判定が二度手間になるため weak で十分なことになります。
; armv8.0a cas-weak (0 -> 3)
ldaxr w2, [x19]
cbnz w2, lbb18
orr w8, wzr, #0x3
stlxr w9, w8, [x19]
b lbb19
lbb18:
clrex
lbb19:
strong では再実行あり。
; armv8.0a cas-strong (3 -> 0)
loop:
ldaxr r2, [x19]
cmp w2, #3
b.ne lbb24
stlxr w8, wzr, [x19]
cbnz w8, loop
orr w1, wzr, #0x1
b lbb25
lbb24:
clrex
lbb25:
x64 と ARMv8.1A はそれぞれ weak/strong の違いがなく 1命令です。
; x64
lock cmpxchgl %ebx, 8(%rsp)
; armv8.1a
casal w2, w3, [x22]
casal は Compare and Swap (CAS) + acq_rel。
● memory_order の違い
load/store それぞれの memory_order 指定による違いは下記の通り。まずは load の場合。
; armv7a
; (relaxed)
ldr r0, [r4]
; (acquire/consume/seq_cst)
ldr r0, [r4]
dmb ish
ARMv8.1 の違いは特になく 8.0 と同じでした。
; armv8.0a/armv8.1a
; (relaxed)
ldr w1, [x19]
; (acquire/consume/seq_cst)
ldar w1, [x19]
store の場合。
; armv7a
; (relaxed)
str r5, [r4]
; (release)
dmb ish
str r5, [r4]
; (seq_cst)
dmb ish
str r5, [r4]
dmb ish
; armv8.0a/armv8.1a
; (relaxed)
str w20, [x19]
; (release/seq_cst)
stlr w20, [x19]
x64 では store( seq_cst ) に xchg が使われている以外は全部 mov です。ARM では厳密に Memory Barrier が挿入されています。
Snapdragon 845 向けに ARMv8.2 バイナリを作ると思ったよりも生成コードに違いがあることがわかりました。iOS では従来の arm64 に加えて arm64e が追加されており、ARM v8.x 拡張命令に対応した新しいバイナリを区別しています。Android では特に区別がないですが、fp16 演算を使った最適化を行う場合は ARMv7 + NEON のときと同じように binary を分けてロードする必要があるかもしれません。
実際の出力などより詳しくは下記のページにまとめています。
関連ページ
・CPU Atomic / Memory Barrier
関連エントリ
・Snapdragon 845 ARMv8.2A 半精度 fp16 演算命令を使ってみる / Deep Learning 命令
・スレッド同期命令の比較 C++11 とコンパイラ