我有一个问题,我刚刚在求职面试中给出了一个问题,我在徘徊数据处理器缓存。问题本身与volatile变量有关,我们怎么能不优化这些变量的内存访问。根据我的理解,当我们读取volatile变量时,我们需要省略处理器缓存。这就是我的问题所在。在这种情况下发生的是,当执行此类变量的访问时,是否刷新了整个缓存?或者有一些寄存器设置,应该省略内存区域的缓存?或者有没有查看内存而不查看缓存的功能?或者它是依赖于架构的。
提前感谢您的时间和答案。
答案 0 :(得分:1)
这里有一些混乱 - 你的程序使用的内存(通过编译器),实际上是一个抽象,由操作系统和处理器一起维护。因此,您不必"需要" 担心分页,交换,物理地址空间和性能。
等等,在你跳起来对我大喊大叫之前没有说话 - 这并不是说你不应该关心它们,在优化你的代码时你可能想知道实际发生了什么,所以你有一套帮助您的工具(例如SW预取),以及关于系统如何工作的大致想法(缓存大小和层次结构),允许您编写优化代码。 但是,正如我所说,你不必担心这一点,如果你不这样做 - 它保证在一定程度上工作......#34;例如,即使在处理共享数据(通过一组相当复杂的硬件协议维护)时,也可以保证高速缓存保持一致性,甚至在虚拟地址别名的情况下(多个virt地址指向相同的物理地址)一)。但是在某种程度上来了#34;部分 - 在某些情况下,您必须确保正确使用它。如果你想为例如内存映射IO,你应该正确定义它,以便处理器知道它不应该被缓存。编译器不可能隐式地为你做这件事,它可能甚至不知道。
现在,volatile
位于上层,它是程序员和编译器之间合同的一部分。这意味着编译器不允许使用此变量进行各种优化,即使在内存模型抽象中,对于程序也是不安全的。这些基本上是可以在任何点从外部修改值的情况(通过中断,mmio,其他线程......)。请记住,编译器仍然存在于内存抽象之上,如果它决定将内容写入内存或读取它,除了可能的提示之外,它完全依赖于处理器来做任何需要的事情来使这块内存近在咫尺而保持正确。但是,允许编译器比HW更自由 - 它可以决定移动读/写或完全消除变量,在大多数情况下CPU是不允许的,所以你需要防止这种情况发生,如果它不安全。可以在此处找到一些很好的例子 - http://www.barrgroup.com/Embedded-Systems/How-To/C-Volatile-Keyword
因此,虽然易失性提示限制了内存模型中编译器的自由度,但它并不一定限制底层HW。你可能不想要它 - 比如你有一个你希望暴露给其他线程的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
- 因为变量被视为&#34;安全&#34;足够的编译器(此处注释掉volatile),它允许自己将其视为循环不变量。事实上,如果我没有在每次迭代中打印一些东西,整个循环就会被优化掉,因为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或其他可能在内存系统下默默改变的东西 - 那么缓存的值将是错误的(那就是&#39; s为什么MMIO不应该被缓存),编译器永远不会知道它,即使它被认为是不稳定的。
顺便说一下 - 使用volatile int x
并直接添加它会产生相同的结果。然后再次 - 制作x或px全局变量也会这样做,原因是 - 编译器会怀疑某人可能可以访问它,因此会采取与显式volatile提示相同的预防措施。有趣的是enuogh,同样适用于制作x local,但将其地址复制到全局指针(但仍然在主循环中直接使用x)。编译器非常谨慎。
这并不是说它100%完全证明,理论上你可以保持x本地,让编译器进行优化,然后猜测&#34;从外面到某处的地址(例如,另一个线程)。这就是当volatile变得派上用场时。
答案 1 :(得分:0)
volatile variable, how can we not optimize the memory access for those variables.
是的,Volatile
on变量告诉编译器可以读取或写入变量,以便程序员可以预见程序范围内的变量会发生什么,编译器无法看到。这意味着编译器无法对变量执行优化,这将改变预期的功能,在寄存器中缓存其值以避免在每次迭代期间使用寄存器副本进行存储器访问。
`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?
显然,当寄存器位于无内存空间时,访问该内存变量将为您提供最新值,而不是来自高速缓存内存。同样,这应该取决于架构。