C言語volatileノート
C volatile note
-v0.1 2018.3.11 Sherlock init -v0.2 2018.3.12 Sherlock finish Westford.
本文はC言語のvolatileキーワードの使い方を紹介する.
C言語ではvolatileは変数を修飾し、コンパイラに変数をコンパイルする方法を教えます.コンパイラは1つの変数をコンパイルして、とてもはっきりしているように見えます:1つの変数はメモリの中で保存して、読むなら対応するメモリを読んで、この変数を書くならこのメモリに値を書けばいいのに、どうしてvolatileという言葉で修飾しますか?
その前に2つの問題を明らかにしなければなりません.
第一に、コードはアセンブリ命令にコンパイルして実行することができ、コンパイルされたアセンブリ命令はメモリからこの値を取るのではなく、もちろんメモリを書くときにすぐに発効しない可能性があります.ここではメモリについてしか言及していませんが、cacheとは言いません.ここでは、コードが実行されているマシンがハードウェアレベルでメモリ/cacheのデータ整合性を保証できると仮定します.つまり、cacheはソフトウェアレベルで透明です.実は今ではほとんどの機械も確かにそうです.
第二に、値を読み書きするとき、対応する物理記憶はメモリ(DDR)に違いないとは思わないでください.それはデバイスのIOレジスタ(MMIO)でもいいです.例えば、物理デバイスのIOレジスタを直接mmapして、ユーザー状態でmmapのアドレスを使って、あなたはこのアドレスにアクセスして、実はこのデバイスのレジスタにアクセスします.IOレジスタの属性はメモリとは全く異なり、1つの値を書くのは1つの操作をトリガーするためであり、同じ値を書くのはもう一度この操作をトリガーするためであり、2つ目の操作は1つ目の操作に代わることはできない.ハードウェアはIOレジスタでステータス、すなわち「メモリ」の値を返すこともでき、自分で変更する可能性があります:)
もう1つのケースは、複数の独立した実行フローが1つの変数を変更する場合と同様である.書き込み操作がメモリの値を変更した場合、読み取りコードがキャッシュの値を読み出す場合、エラーが発生します.ここでは、cacheとメモリが一致しないという問題は含まれていないことを改めて強調します.つまり、メモリが更新され、読むときに、対応するcacheを読んでも、ハードウェアはcacheのデータとメモリのデータが異なることを認識し、ハードウェアは自動的にメモリの新しいデータをcacheに記入し、その後読むと正しい値を読むことができます.では、読み取り操作がレジスタにデータを遅らせると、いつ問題が発生しますか.
以上の認識に基づいて、コードを見てみましょう.
/* for test 1 */
int test;
/* for test 2 */
volatile int test_v;
/* for test 3 */
int test_3, tmp1, tmp2, tmp3;
void volatile_test_1()
{
test = 111;
test = 222;
test = 333;
}
void volatile_test_2()
{
test_v = 111;
test_v = 222;
test_v = 333;
}
void volatile_test_3_read()
{
test = test_3;
tmp1 = test_3;
tmp2 = test_3;
tmp3 = test_3;
}
void volatile_test_3_write()
{
test_3 = 111;
}
このコードが通常の場合、コンパイルされたアセンブリコードは:(x 86-arm 64 gccクロスコンパイラを使用)
.arch armv8-a
.file "test.c"
.comm test,4,4
.comm test_v,4,4
.comm test_3,4,4
.comm tmp1,4,4
.comm tmp2,4,4
.comm tmp3,4,4
.text
.align 2
.global volatile_test_1
.type volatile_test_1, %function
volatile_test_1:
adrp x0, test
add x0, x0, :lo12:test
mov w1, 111
str w1, [x0]
adrp x0, test
add x0, x0, :lo12:test
mov w1, 222
str w1, [x0]
adrp x0, test
add x0, x0, :lo12:test
mov w1, 333
str w1, [x0]
nop
ret
.size volatile_test_1, .-volatile_test_1
.align 2
.global volatile_test_2
.type volatile_test_2, %function
volatile_test_2:
adrp x0, test_v
add x0, x0, :lo12:test_v
mov w1, 111
str w1, [x0]
adrp x0, test_v
add x0, x0, :lo12:test_v
mov w1, 222
str w1, [x0]
adrp x0, test_v
add x0, x0, :lo12:test_v
mov w1, 333
str w1, [x0]
nop
ret
.size volatile_test_2, .-volatile_test_2
.align 2
.global volatile_test_3_read
.type volatile_test_3_read, %function
volatile_test_3_read:
adrp x0, test_3
add x0, x0, :lo12:test_3
ldr w1, [x0]
adrp x0, test
add x0, x0, :lo12:test
str w1, [x0]
adrp x0, test_3
add x0, x0, :lo12:test_3
ldr w1, [x0]
adrp x0, tmp1
add x0, x0, :lo12:tmp1
str w1, [x0]
adrp x0, test_3
add x0, x0, :lo12:test_3
ldr w1, [x0]
adrp x0, tmp2
add x0, x0, :lo12:tmp2
str w1, [x0]
adrp x0, test_3
add x0, x0, :lo12:test_3
ldr w1, [x0]
adrp x0, tmp3
add x0, x0, :lo12:tmp3
str w1, [x0]
nop
ret
.size volatile_test_3_read, .-volatile_test_3_read
.align 2
.global volatile_test_3_write
.type volatile_test_3_write, %function
volatile_test_3_write:
adrp x0, test_3
add x0, x0, :lo12:test_3
mov w1, 111
str w1, [x0]
nop
ret
.size volatile_test_3_write, .-volatile_test_3_write
.ident "GCC: (Linaro GCC 6.3-2017.05) 6.3.1 20170404"
.section .note.GNU-stack,"",@progbits
しかし、-O 3のオプションを加えて、コンパイラにコンパイル結果の最適化を手伝ってもらいました.最後のコードは:
.arch armv8-a
.file "test.c"
.text
.align 2
.p2align 3,,7
.global volatile_test_1
.type volatile_test_1, %function
volatile_test_1:
adrp x0, test
mov w1, 333
str w1, [x0, #:lo12:test]
ret
.size volatile_test_1, .-volatile_test_1
.align 2
.p2align 3,,7
.global volatile_test_2
.type volatile_test_2, %function
volatile_test_2:
adrp x0, test_v
mov w3, 111
mov w2, 222
mov w1, 333
str w3, [x0, #:lo12:test_v]
str w2, [x0, #:lo12:test_v]
str w1, [x0, #:lo12:test_v]
ret
.size volatile_test_2, .-volatile_test_2
.align 2
.p2align 3,,7
.global volatile_test_3_read
.type volatile_test_3_read, %function
volatile_test_3_read:
adrp x0, test_3
adrp x4, test
adrp x3, tmp1
adrp x2, tmp2
adrp x1, tmp3
ldr w0, [x0, #:lo12:test_3]
str w0, [x4, #:lo12:test]
str w0, [x3, #:lo12:tmp1]
str w0, [x2, #:lo12:tmp2]
str w0, [x1, #:lo12:tmp3]
ret
.size volatile_test_3_read, .-volatile_test_3_read
.align 2
.p2align 3,,7
.global volatile_test_3_write
.type volatile_test_3_write, %function
volatile_test_3_write:
adrp x0, test_3
mov w1, 111
str w1, [x0, #:lo12:test_3]
ret
.size volatile_test_3_write, .-volatile_test_3_write
.comm tmp3,4,4
.comm tmp2,4,4
.comm tmp1,4,4
.comm test_3,4,4
.comm test_v,4,4
.comm test,4,4
.ident "GCC: (Linaro GCC 6.3-2017.05) 6.3.1 20170404"
.section .note.GNU-stack,"",@progbits
通常のコンパイルでは問題ないことがわかります.問題は、コンパイラが最適化するときに発生します.
test_1変数を何度も書くとき、コンパイラは最後に書けばいいと思っています.もし、test 1がIOレジスタ、またはtest_に対応している場合1の値は、別の実行フローで認識され、ここで問題が発生します.解決策はtest_に1 volatileを加えると、コンパイラは書くたびにコンパイラに最適化されないことを教えます.
test_3 1つの変数を複数回読む場合、コンパイラ後の2回の読み取りは、メモリを正読みするのではなく、レジスタに最初にキャッシュされた値を使用します.もし、test_3対応はIOレジスタ、またはコードのようにtest_3は別の実行フローで変更されていますが(volatile_test_3_write)、read操作は読み出しレジスタの値で、問題が発生します.修正するにもvolatileを追加します.
上記の第1では、メモリを書くときはすぐに有効になるわけではありませんが、ここでもcacheとは関係ありません.ハードウェアがcacheの一貫性を保証するシステムでは、単一の操作に対して、メモリを書くとすぐに有効になると考えられます.ここでいう不発効とは,CPUの複数の書き込み操作を書き込み完了の前後が保証されていないことである.書き込み操作の優先順位を保証するには、メモリバリアコマンドを使用する必要があります.これは本問討論の問題ではないから,別に話しましょう.ここでは、volatileはコンパイラに追加の最適化を行わないように伝えるだけで、書き込みの有効な順序を保証することはできません.