ここにはいくつかの混乱があります。プログラムが (コンパイラを介して) 使用するメモリは、実際には抽象化されており、OS とプロセッサによって一緒に維持されます。そのため、ページング、スワッピング、物理アドレス空間、およびパフォーマンスについて心配する必要はありません。
コードを最適化するときに、実際に何が起こるかを知りたいと思うかもしれないので、あなたを支援する一連のツールがあります( SW のプリフェッチなど) と、システムがどのように機能するか (キャッシュのサイズと階層) に関する大まかなアイデアを提供し、最適化されたコードを記述できるようにします。しかし、私が言ったように、あなたはこれについて心配する必要はありません. たとえば、キャッシュは、共有データ (一連の非常に複雑な HW プロトコルによって維持される) を操作する場合や、仮想アドレス エイリアス (同じ物理アドレスを指す複数の仮想アドレス) の場合でも一貫性を維持することが保証されます。しかし、ここで「ある程度」が来ます 一部 - 場合によっては、正しく使用していることを確認する必要があります。たとえば、メモリにマップされた IO を実行する場合は、適切に定義して、プロセッサがキャッシュされるべきではないことを認識できるようにする必要があります。コンパイラが暗黙的にこれを行う可能性は低く、おそらく知らないでしょう。
現在、volatile
上位レベルに住んでいます。これは、プログラマーとコンパイラーの間の契約の一部です。これは、コンパイラがこの変数を使用してあらゆる種類の最適化を行うことを許可されていないことを意味します。これは、メモリ モデルの抽象化内であっても、プログラムにとって安全ではありません。. これらは基本的に、(割り込み、mmio、他のスレッドなどを介して) 任意の時点で値を外部から変更できる場合です。コンパイラーは、メモリに何かを書き込んだり読み取ったりすることを決定した場合、メモリの抽象化の上にまだ存在することに注意してください。可能なヒントは別として、このメモリのチャンクを手元に近づけるために必要なことはすべてプロセッサに完全に依存しています。正しさを保つ。ただし、コンパイラーは HW よりもはるかに自由度が高く、読み取り/書き込みを移動したり、変数をまとめて削除したりすることができますが、これはほとんどの場合 CPU では許可されていないため、安全でない場合はそれを防止する必要があります。 . それが起こるときのいくつかの良い例はここにあります - http://www.barrgroup.com/Embedded-Systems/How-To/C-Volatile-Keyword
したがって、揮発性ヒントはメモリ モデル内のコンパイラの自由を制限しますが、基になるハードウェアを必ずしも制限するわけではありません。あなたはおそらくそれを望んでいないでしょう - あなたが他のスレッドに公開したい揮発性変数を持っているとしましょう - コンパイラがそれをキャッシュ不可にすると、パフォーマンスが損なわれます (そして必要なしに)。それに加えて、メモリ モデルを安全でないキャッシュから保護したい場合 (これは volatile が役立つケースのサブセットにすぎません)、明示的に保護する必要があります。
編集:例を追加しないのは気分が悪いので、わかりやすくするために、次のコードを検討してください。
int main() {
int n = 20;
int sum = 0;
int x = 1;
/*volatile */ int* px = &x;
while (sum < n) {
sum+= *px;
printf("%d\n", sum);
}
return 0;
}
これは、1 である x のジャンプで 1 から 20 までカウントされますgcc -O3
。
0000000000400440 <main>:
400440: 53 push %rbx
400441: 31 db xor %ebx,%ebx
400443: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
400448: 83 c3 01 add $0x1,%ebx
40044b: 31 c0 xor %eax,%eax
40044d: be 3c 06 40 00 mov $0x40063c,%esi
400452: 89 da mov %ebx,%edx
400454: bf 01 00 00 00 mov $0x1,%edi
400459: e8 d2 ff ff ff callq 400430 <__printf_chk@plt>
40045e: 83 fb 14 cmp $0x14,%ebx
400461: 75 e5 jne 400448 <main+0x8>
400463: 31 c0 xor %eax,%eax
400465: 5b pop %rbx
400466: c3 retq
注意してくださいadd $0x1,%ebx
-変数はコンパイラによって十分に「安全」と見なされているため(ここでは揮発性がコメントアウトされています)、ループ不変と見なすことができます。実際、各反復で何かを印刷していなければ、gcc は最終結果を非常に簡単に判断できるため、ループ全体が最適化されていたはずです。
ただし、 volatile キーワードのコメントを外すと、次のようになります -
0000000000400440 <main>:
400440: 53 push %rbx
400441: 31 db xor %ebx,%ebx
400443: 48 83 ec 10 sub $0x10,%rsp
400447: c7 04 24 01 00 00 00 movl $0x1,(%rsp)
40044e: 66 90 xchg %ax,%ax
400450: 8b 04 24 mov (%rsp),%eax
400453: be 4c 06 40 00 mov $0x40064c,%esi
400458: bf 01 00 00 00 mov $0x1,%edi
40045d: 01 c3 add %eax,%ebx
40045f: 31 c0 xor %eax,%eax
400461: 89 da mov %ebx,%edx
400463: e8 c8 ff ff ff callq 400430 <__printf_chk@plt>
400468: 83 fb 13 cmp $0x13,%ebx
40046b: 7e e3 jle 400450 <main+0x10>
40046d: 48 83 c4 10 add $0x10,%rsp
400471: 31 c0 xor %eax,%eax
400473: 5b pop %rbx
400474: c3 retq
400475: 90 nop
add オペランドがスタックから読み込まれるようになりました。これは、コンパイラが誰かがそれを変更したのではないかと疑うようになるためです。それはまだキャッシュであり、通常のライトバック タイプのメモリと同様に、別のスレッドまたは DMA からの変更の試みをキャッチし、メモリ システムが新しい値を提供します (ほとんどの場合、キャッシュ ラインはスヌープされて無効化され、CPU現在所有しているコアから新しい値を取得します)。ただし、私が言ったように、x が通常のキャッシュ可能なメモリ アドレスではなく、何らかの MMIO か、メモリ システムの下で静かに変更される可能性のある何かである場合は、キャッシュされた値が間違っていることになります (そのため、MMIO を使用する必要があります)。 t はキャッシュされません)、たとえそれが揮発性と見なされていても、コンパイラはそれを決して知りません。
ちなみに、volatile int x
直接使用して追加しても同じ結果になります。繰り返しになりますが、x または px グローバル変数を作成することもできます。その理由は、コンパイラーは、誰かがアクセスした可能性があると推測するため、明示的な volatile ヒントの場合と同じ予防措置を講じるからです。興味深いことに、同じことが x をローカルにする場合にも当てはまりますが、そのアドレスをグローバル ポインターにコピーします (ただし、メイン ループでは x を直接使用します)。コンパイラは非常に慎重です。100% 完全な証明であるとは言えません。理論的には x をローカルに保持し、コンパイラに最適化を実行させてから、外部からどこかのアドレスを「推測」することができます (たとえば、別のスレッド)。これは、揮発性が重宝するときです。