前几天测试空循环速度时发生了一些匪夷所思的事情,经过调查,我锁定了凶手:关键字volatile
编译器:VS2013
语言:C/C++
关于这个关键字,简单来说就是通过保证内存的可见性来防止编译器的过度优化。
这么专业的一句话说出来,我却更加的迷惑了:什么是内存的可见性?为什么要防止编译器的过度优化?
说起来,这其实是同一个问题: 编译器太“聪明”了1
2
3
4
5
6
7
8void test()
{
int i = 0;
i = 1;
i = 2;
i = 3;
i = 4;
}
这样一个函数,有五句代码,但是编译器会“觉得”i = 4;所以前面的语句其实并没有转换成为机器码(仅release),这其实在一般的情形下并没有什么区别,还提高了效率。但是并不符合我们编写时的想法,所以加上volatile后编译器就老老实实生成多条指令了。
内存可见性:
- 线程内,当读取一个变量时,为提高存取速度,编译器优化时有时会先把变量读取到一个寄存器中;以后再取变量值时,就直接从寄存器中取值;
- 当变量值在本线程里改变时,会同时把变量的新值copy到该寄存器中,以便保持一致
- 当变量在因别的线程等而改变了值,该寄存器的值不会相应改变,从而造成应用程序读取的值和实际的变量值不一致
- 当该寄存器在因别的线程等而改变了值,原变量的值不会改变,从而造成应用程序读取的值和实际的变量值不一致
几个例子
也就是说,我们不加上这个关键字,可能会出现一些让我们费解的现象
- 空循环速度
1
2
3
4
5
6
7
8
9void test_loop()
{
clock_t c1 = clock();
for (int i = 0; i < 1000000000; i++);
clock_t c2 = clock();
for (volatile int i = 0; i < 1000000000; i++);
clock_t c3 = clock();
cout << c2 - c1 << endl << c3 - c2<<endl;
}
在debug模式下,我们看一看输出:
换成release再试一次:
很明显在release下第一个循环被优化掉了,速度快到飞起~
- 变量值被意外改变(其实是故意~)
1
2
3
4
5
6
7
8
9
10
11
12void test_ebp()
{
int i = 10;
int a = i;
printf("i=%d\n", a);
__asm
{
mov dword ptr[ebp - 8], 10h//“偷偷改变i的值”
}//VS2013下i的地址是ebp - 8,其他编译器可能不一致,如VC++6.0下应该是ebp - 4
int b = i;
printf("i=%d\n", b);
}
我们看看debug下的输出:
再看看release:
很明显读取i值是直接从寄存器里读取了,所以结果居然一致
给i加上volatile果然结果就都一致了:
- 多线程下volatile带来的不可思议的结果
我们先看下面的函数:1
2
3
4int square(volatile int* &p)
{
return (*p)*(*p);
}
看起来是求一个变量平方的函数,但是偏偏是经过volatile修饰的,从而在编译器眼里这个函数是这样的:1
2
3
4
5
6int square(volatile int* &p)
{
int a = *p;
int b = *p;
return a*b;
}
如果不在多线程环境里,无非是让编译器多忙了几句,但是一旦在多线程下,其他线程可能会在a,b取值之间改变了*p的值,从而得出错误的结果:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37int A = 100;
volatile int *PA = &A;
int square(volatile int* &p)
{
return (*p)*(*p);
}
DWORD WINAPI test(LPVOID lpParamter)
{
int ans = square2(PA);
printf("%s %d\n","pthread 1:",ans);
for (int i = 0; i < 4200000000; i++)
{
if (i*i == ans)
{
cout << "right" << endl;
return 0;
}
}
cout << "wrong" << endl;
return 0;
}
DWORD WINAPI test2(LPVOID lpParamter)
{
int i = 0;
while (1)
{
*PA = i++;
}
return 0;
}
void test_pthread()
{
HANDLE hThread2 = CreateThread(NULL, 0, test2, NULL, 0, NULL);
HANDLE hThread = CreateThread(NULL, 0, test, NULL, 0, NULL);
CloseHandle(hThread);
CloseHandle(hThread2);
}
在test函数里会对函数的出来的结果进行检测,判断是不是一个数的平方(即使溢出也没事)
不过我要说的是:即便是在一个线程里不断改变*P的值,出错率还是挺低的~