C言語volatileノート

13279 ワード

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はコンパイラに追加の最適化を行わないように伝えるだけで、書き込みの有効な順序を保証することはできません.