考虑这个C代码:
extern volatile int hardware_reg;
void f(const void *src, size_t len)
{
void *dst = <something>;
hardware_reg = 1;
memcpy(dst, src, len);
hardware_reg = 0;
}
memcpy()
调用必须在两个任务之间进行。通常,由于编译器可能不知道被调用函数将执行什么操作,因此它无法将对函数的调用重新排序为在赋值之前或之后。但是,在这种情况下,编译器知道函数将执行什么操作(甚至可以插入内联内置替换),并且可以推断出memcpy()
永远不能访问hardware_reg
。在我看来,编译器在移动memcpy()
调用时会遇到麻烦,如果它想这样做的话。
所以,问题是:单独的函数调用是否足以发出阻止重新排序的内存屏障,或者在调用memcpy()
之前和之后是否需要显式内存屏障?
如果我误解了事情,请纠正我。
编译器无法在memcpy()
之前或hardware_reg = 1
之后重新排序hardware_reg = 0
操作 - 这就是volatile
将确保的 - 至少就编译器发出的指令流而言。函数调用不一定是“内存屏障”,但它是一个序列点。
C99标准对volatile
(5.1.2.3/5“程序执行”)说了这个:
在序列点处,易失性对象在先前访问完成且后续访问尚未发生的意义上是稳定的。
因此,在由memcpy()
表示的序列点处,必须发生写1
的易失性访问,并且不能发生写0
的易失性访问。
但是,有两件事我想指出:
<something>
的不同,如果目标缓冲区没有其他任何操作,编译器可能能够完全删除memcpy()
操作。这就是微软提出SecureZeroMemory()
功能的原因。 SecureZeroMemory()
使用volatile
限定指针来防止优化写入。volatile
并不一定意味着内存屏障(这是一个硬件的东西,而不仅仅是代码排序的东西),所以如果你在多进程机器或某些类型的硬件上运行,你可能需要显式调用内存屏障(也许是Linux上的wmb()
)。
从MSVC 8(VS 2005)开始,Microsoft记录volatile
关键字意味着适当的内存屏障,因此可能不需要单独的特定内存屏障调用:
http://msdn.microsoft.com/en-us/library/12a04hfd.aspx
此外,在优化时,编译器必须维护对易失性对象的引用之间的顺序以及对其他全局对象的引用。特别是,
对volatile对象的写入(volatile write)具有Release语义;对在指令序列中写入易失性对象之前发生的全局或静态对象的引用将在编译二进制文件中的易失性写入之前发生。
读取volatile对象(volatile read)具有Acquire语义;在读取编译二进制文件中的易失性读取之后,将在读取指令序列中的易失性存储器之后发生对全局或静态对象的引用。据我所知,你的推理导致了
编译器在移动
memcpy
调用时会遇到麻烦
是正确的。语言定义没有回答您的问题,只能参考特定的编译器来解决。
很抱歉没有更多有用的信息。
我的假设是编译器永远不会重新命令volatile赋值,因为它必须假设它们必须在代码中出现的位置完全执行。
由于编译器会内联mecpy调用并消除第一个赋值,或者因为它被编译为RISC代码或机器代码并在那里得到优化,因此它将进行优化。
这是一个稍微修改过的示例,在x86-64上使用gcc 7.2.1编译:
#include <string.h>
static int temp;
extern volatile int hardware_reg;
int foo (int x)
{
hardware_reg = 0;
memcpy(&temp, &x, sizeof(int));
hardware_reg = 1;
return temp;
}
gcc知道memcpy()
与赋值相同,并且知道temp
不能在其他地方访问,因此temp
和memcpy()
完全从生成的代码中消失:
foo:
movl $0, hardware_reg(%rip)
movl %edi, %eax
movl $1, hardware_reg(%rip)
ret