考虑这个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()
之前和之后是否需要显式内存屏障?< / p>
如果我误解了事情,请纠正我。
答案 0 :(得分:11)
编译器无法在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
关键字意味着适当的内存屏障,因此可能不需要单独的特定内存屏障调用:
另外,在优化时,编译器 必须保持秩序 也引用了volatile对象 作为对其他全局对象的引用。 特别是,
对volatile对象的写入(volatile write)具有Release 语义;对全球或全球的引用 在a之前发生的静态对象 写入中的volatile对象 指令序列将在之前发生 那个volatile编译在编译中 二进制
读取volatile对象(volatile read)具有Acquire语义; 对全局或静态的引用 读取后发生的对象 指令中的易失性存储器 之后将发生序列 在已编译的二进制文件中读取volatile。
答案 1 :(得分:3)
据我所知,你的理由导致
编译器在移动
时会遇到麻烦memcpy
调用
是对的。语言定义没有回答您的问题,只能参考特定的编译器来解决。
很抱歉没有更多有用的信息。
答案 2 :(得分:0)
我的假设是编译器永远不会重新命令volatile赋值,因为它必须假设它们必须在代码中出现的位置完全执行。
答案 3 :(得分:0)
它是probalby将被优化,或者是因为编译器内联mecpy调用并消除了第一个赋值,或者因为它被编译为RISC代码或机器代码并在那里得到优化。
答案 4 :(得分:0)
这是一个稍微修改过的示例,在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