作 者: HUFAN 标 题: volatile和原子操作 时 间: Thu Aug 6 12:16:53 2009 点 击: 25 所谓原子操作,就是"不可中断的一个或一系列操作" , 在确认一个操作是原子的情况下, 多线程环境里面,我们可以避免仅仅为保护这个操作在外围加上性能昂贵的锁,甚至借助 于原子操作,我们可以实现互斥锁。 很多操作系统都为int类型提供了+-赋值的原子操作版本,比如 NT 提供了 InterlockedExchange 等API, Linux/UNIX也提供了atomic_set 等函数。 前两天有同学问我:在x86上,g_count++ (int类型) 是否是一个原子操作? 我的回答是 "不是的, 多个CPU的机器(SMP)上面这就不是原子操作"。 今天想起,在单CPU上这个是否是原子操作呢,但是这个和编译器有关,编译器可能有两种 编译方式: A. 多条指令版本 , 这就不是原子的 MOV 寄存器 , g_count ADD 寄存器, 1 MOV g_count , 寄存器 B. 单指令版本, 这在单CPU的x86上就是原子的 INC g_count 只能写程序验证了, 让5个线程每个对 g_count++ 一亿次,假如是原子操作的话,结果应 该是5亿: 其实还需要对 g_count 进行volatile声明,防止编译器对这里不适当的优化,为了看看编 译器对volatile的处理,我另外做了个volatile版本作为比较。 #include <windows.h> #include <stdio.h> int g_count = 0; DWORD WINAPI ThreadFunc( LPVOID lpParam ) { int i; printf( "Thread %d start/n", (DWORD*)lpParam ); for (i=0; i <100000000 ; i++) g_count++; printf( "Thread %d quit/n", (DWORD*)lpParam ); return 0; } #define THREAD_NUM 5 VOID main( VOID ) { DWORD dwThreadId; HANDLE hThread; int i; for (i=0;i<THREAD_NUM;i++) { hThread = CreateThread( NULL, // default security attributes 0, // use default stack size ThreadFunc, // thread function (LPVOID)i, // argument to thread function 0, // use default creation flags &dwThreadId); // returns the thread identifier // Check the return value for success. if (hThread == NULL) { printf( "CreateThread failed./n" ); } } printf("Press any key after all thread exit.../n"); getchar(); printf("g_count %d/n", g_count); if (g_count!=THREAD_NUM*100000000) { printf("ERROR! g_count %d!=%d/n", g_count, THREAD_NUM*100000000); } getchar(); //一个随手的程序,就不close handle了 } volatile的本意是易变的, 它限制编译器的优化,因为CPU对寄存器处理比内存快很多,我 想这个程序的没有加上volatile的版本优化以后应该是这样: MOV 寄存器, g_count for循环一亿次, 执行 INC 寄存器 MOV g_count, 寄存器 这样,最后g_count的值应该是1亿,2亿,3亿,4亿,5亿的整数,1亿出现的可能性较高。 而加上volatile以后,或者是没有代码优化的版本,都是老老实实对内存加上一亿次,假 如不是原子操作的话,最后结果就会比五亿小。 用的是Vc6的cl编译器,我预期的结果是这样的: ++是原子操作 没有代码优化 代码优化(cl -O2编译) 没有 volatile g_count == 五亿 g_count的值应该是1亿,2亿,3亿,4亿,5亿的整数 volatile g_count == 五亿 g_count == 五亿 ++ 不是原子操作 没有代码优化 代码优化(cl -O2编译) 没有 volatile g_count < 五亿 g_count的值应该是1亿,2亿,3亿,4亿,5亿的整数,1亿出现的可能性较高 volatile 同上 g_count < 五亿 但是最后的结果却让我大跌了一下眼镜: VC6实验的结果 没有代码优化 代码优化 没有 volatile g_count 一般为五亿, 偶尔< 五亿(疑惑中...) 都是五亿(疑惑中...) volatile 同上(疑惑中...) g_count = < 五亿(这个可以解释) 这个结果太让人疑惑了,没办法,只能看asm代码了, 首先看看为什么volatile的版本为什 么和预期不符合吧: 这里是没有优化的版本(编译命令行 cl -Fa test_thread.c): for (i=0; i <100000000 ; i++) 初始化i=0; mov DWORD PTR _i$[ebp], 0 jmp SHORT $L52751 $L52752: i++ mov ecx, DWORD PTR _i$[ebp] add ecx, 1 mov DWORD PTR _i$[ebp], ecx $L52751: 判断 i <100000000 cmp DWORD PTR _i$[ebp], 100000000 ; 05f5e100H jge SHORT $L52753 g_count++; //这里发现编译使用的是多个指令,也就是说g_count++不是原子的 mov edx, DWORD PTR _g_count add edx, 1 mov DWORD PTR _g_count, edx jmp SHORT $L52752 下面是加了volatile的优化版本(编译命令行 cl -Fa test_thread.c -O2) //初始化 i = 100000000, 这个循环变量被直接放到了寄存器里面 mov eax, 100000000 ; 05f5e100H $L52793: //g_count++;这里发现编译使用的是多个指令,也就是说g_count++不是原子的 mov ecx, DWORD PTR _g_count inc ecx mov DWORD PTR _g_count, ecx //下面又是循环体的asm代码 dec eax // i-- jne SHORT $L52793 // if (i>0) 则继续循环 终于发现了问题所在了, 优化以后,循环从i++变成了i--, 就是如下的形式: for (i=100000000; i >0 ; i--) g_count++; 因为将一个数字和0比较和将其与其他数字比较更加有效率优势,而且这里i在循环体里面 并不使用,所以VC编译器将其变换成上面的形式,可以大大节省循环运行的时钟周期。 这样,未优化的版本有很大的机会出现 g_count == 五亿 就有了解释,是因为: CPU对于纯粹的整数运算是很快的,一亿次循环里面,可能只有一两次的线程上下文切换 没有优化的版本循环体比++操作本身更加耗时,这样切换操作很可能出现在 for 循环中, 而不是 g_count++ 的三条指令之间 这里也证明了VC6编译器对于 ++ 的运行代码是是非原子的,查了一下资料 这3条指令在 pentium以后的CPU比一条inc更快 然后再检查没有加volatile的优化版本 发现汇编代码的循环体完全没有了: mov eax, DWORD PTR _g_count push esi add eax, 100000000 ; 05f5e100H 表示成C的代码大概就是这样: g_count+=100000000; 编译器还是很聪明,发现这个循 环其实使用前面的语句也可以达到目的,干脆把循环拿掉了,这样因为线程执行时间很短 ,往往一个线程都执行完了其他线程还没有被调度,所以结果都是5亿了。 附带以下总结: 1. 不要小看编译器的聪明程度,上面的那些优化,我在gcc上也作了验证,我们不要太在 意i++/++i之类的优化,要相信编译器能做好它 2. ++的操作在单CPU的x86上也不是原子性的,所以优化多线程性能的兄弟不要在这里搞过 火,老实用 InterlockedIncrement 吧 3. x86上,不管是否SMP, 对于int(要求地址4 bytes对齐)的读取和赋值还是原子的,不过 这个就和这个试验无关了(RISC的机器就不要这样做了,大家还是加锁吧)
volatile和原子操作
最新推荐文章于 2024-10-14 08:41:52 发布