以前我一直认为C的volatile具有一些“平台相关”的语义,比如,与并发访问相关的栅栏。看来只有C#的volatile才有那样的语义。MS实现的C编译器连volatile操作的位置也不再保证,而是保有为了优化可以调动其位置的权利。
像下面这样的代码(VS2010 VC10编译器):
#include <Windows.h>
#include <stdio.h>
static volatile int a;
static volatile int b;
__declspec(noinline) static void f( int *p )
{
b = 7;
a = *p;
}
int main()
{
a = 0;
b = 0;
__try
{
f((int *)a);
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
printf("execpt\n");
}
printf("b=%d\n", b);
getchar();
return 0;
}
从C语言层面讲是在a = *p;处发生异常。但测试结果显示b=0,即没有执行b = 7;就产生了异常。
问题在于,VC10编译器将b = 7;的生成的代码调动到a = *p;产生的代码之间。机器生成的代码类似下面:
00401000 mov ecx,dword ptr [eax]
00401005 mov dword ptr [__fmode+4 (40336Ch)],7
0040100F mov dword ptr [__fmode+8 (403370h)],eax
其中地址00401005为b = 7;生成的代码,而其被a = *p;生成的代码包围住。异常发生在00401000,即取地址0的值。
要想让最后b=7,就要改写f:
__declspec(noinline) static void f( int *p )
{
b = 7;
_ReadWriteBarrier();
a = *p;
}
在两句话之间加入一个读写栅栏。
其实,这是一个伪栅栏(真栅栏是体系结构的机器指令),它并不生成任何代码,只是告诉编译器:在它上下两边的代码要分割开,不能跨越它。
这是个奇怪的函数,显然编译器认识他,看见他,就做了这样一个特殊的工作,这是用户实现的函数所无法做到的。
在MS的实现里,volatile的语义十分单纯,仅仅指“取消寄存器缓冲优化”(或者说,必须实际读/写内存),而不取消“重排列代码”的优化。
(想测试本篇的例子,别忘记要编译Release版本,并用O2优化。)