前段时间在求职面试中,我有一个问题,我在数据处理器缓存上徘徊。问题本身与 volatile 变量有关,我们如何不优化这些变量的内存访问。根据我的理解,当我们读取 volatile 变量时,我们需要省略处理器缓存。这就是我的问题所在。在这种情况下会发生什么,当执行对此类变量的访问时,是否会刷新整个缓存?或者有一些寄存器设置应该为内存区域省略缓存?或者有没有在不查看缓存的情况下读取内存的功能?还是它依赖于架构。
提前感谢您的时间和答案。
前段时间在求职面试中,我有一个问题,我在数据处理器缓存上徘徊。问题本身与 volatile 变量有关,我们如何不优化这些变量的内存访问。根据我的理解,当我们读取 volatile 变量时,我们需要省略处理器缓存。这就是我的问题所在。在这种情况下会发生什么,当执行对此类变量的访问时,是否会刷新整个缓存?或者有一些寄存器设置应该为内存区域省略缓存?或者有没有在不查看缓存的情况下读取内存的功能?还是它依赖于架构。
提前感谢您的时间和答案。
这里有一些混淆 - 您的程序使用的内存(通过编译器)实际上是一种抽象,由操作系统和处理器共同维护。因此,您“不需要”担心分页、交换、物理地址空间和性能。
等等,在你跳起来对我大喊大叫之前 - 这并不是说你不应该关心它们,在优化你的代码时你可能想知道实际发生了什么,所以你有一套工具可以帮助你(例如 SW 预取),以及关于系统如何工作的粗略概念(缓存大小和层次结构),允许您编写优化的代码。但是,正如我所说,您不必担心这一点,如果您不这样做 - 它可以保证在一定程度上“在幕后”工作。例如,即使在处理共享数据(通过一组相当复杂的硬件协议维护)时,甚至在虚拟地址别名(多个虚拟地址指向同一个物理地址)的情况下,缓存也可以保证保持一致性。但这里来了“在一定程度上” 部分 - 在某些情况下,您必须确保正确使用它。例如,如果你想做内存映射 IO,你应该正确定义它,以便处理器知道它不应该被缓存。编译器不太可能隐式地为您执行此操作,它甚至可能都不知道。
现在,volatile
生活在上层,它是程序员和他的编译器之间契约的一部分。这意味着编译器不允许对该变量进行各种优化,即使在内存模型抽象中,这对程序也是不安全的. 这些基本上是可以在任何时候从外部修改值的情况(通过中断,mmio,其他线程,...)。请记住,编译器仍然存在于内存抽象之上,如果它决定将某些内容写入内存或读取它,除了可能的提示之外,它完全依赖处理器来做任何它需要做的事情来使这块内存近在咫尺,而保持正确性。但是,编译器比硬件有更多的自由——它可以决定一起移动读/写或消除变量,在大多数情况下 CPU 是不允许这样做的,所以如果它不安全,你需要防止这种情况发生. 可以在此处找到有关何时发生这种情况的一些很好的示例 - http://www.barrgroup.com/Embedded-Systems/How-To/C-Volatile-Keyword
因此,虽然 volatile 提示限制了内存模型中编译器的自由度,但它不一定限制底层硬件。您可能不希望它 - 假设您有一个想要暴露给其他线程的 volatile 变量 - 如果编译器使其不可缓存,它将破坏性能(并且不需要)。如果最重要的是,您还想保护内存模型免受不安全缓存的影响(这只是 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;
}
这将在 x 的跳跃中从 1 计数到 20,即 1。让我们看看它是如何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
现在正在从堆栈中读取添加操作数,因为编译器会怀疑有人可能会更改它。它仍然是缓存,作为普通的回写类型内存,它会捕获任何从另一个线程或 DMA 修改它的尝试,并且内存系统将提供新值(很可能缓存行会被窥探并失效,从而迫使 CPU从现在拥有它的任何核心获取新值)。但是,正如我所说,如果 x 不应该是一个正常的可缓存内存地址,而是应该是某个 MMIO 或其他可能在内存系统下静默变化的东西 - 那么缓存值将是错误的(这就是为什么 MMIO 应该' t 被缓存),编译器永远不会知道这一点,即使它被认为是易失的。
顺便说一句 -volatile int x
直接使用和添加它会产生相同的结果。再说一遍 - 制作 x 或 px 全局变量也会这样做,原因是 - 编译器会怀疑有人可能可以访问它,因此会采取与显式 volatile 提示相同的预防措施。有趣的是,将 x 设为本地也是如此,但将其地址复制到全局指针中(但仍直接在主循环中使用 x)。编译器相当谨慎。这并不是说它是 100% 的完全证明,理论上您可以将 x 保留在本地,让编译器进行优化,然后从外部某处“猜测”地址(例如,另一个线程)。这是 volatile 派上用场的时候。
volatile variable, how can we not optimize the memory access for those variables.
是Volatile
的,on variable 告诉编译器可以读取或写入该变量,以便程序员可以预见该变量在程序范围之外会发生什么并且编译器无法看到。这意味着编译器无法对将改变预期功能的变量执行优化,将其值缓存在寄存器中以避免在每次迭代期间使用寄存器副本进行内存访问。
`entire cache being flushed when the access for such variable is executed?`
不。理想情况下,编译器从变量的存储位置访问变量,不会刷新 CPU 和内存之间的现有缓存条目。
Or there is some register setting that caching should be omitted for a memory region?
显然,当寄存器位于未缓存的内存空间中时,访问该内存变量将为您提供最新的值,而不是从缓存内存中获得的值。同样,这应该取决于架构。