链接地址:http://hi.baidu.com/chenzhuoyou/blog/item/ff65da1327a6c50a5aaf53e8.html
---------------------------------------------------------------------------------------------------------------------
所谓原子操作,就是"不可中断的一个或一系列操作" , 在确认一个操作是原子的情况下,多线程环境里面,我们可以避免仅仅为保护这个操作在外围加上性能昂贵的锁,甚至借助于原子操作,我们可以实现互斥锁。
很多操作系统都为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更快
发现汇编代码的循环体完全没有了:
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的原子性做探究,加上自己的实验代码和实验结果! Case多核?单核?是否有volatile是否编译器优化-O2结果!
const和volatile分析 这个分析得很好 面试 找工作 必备的
C/C++牛人Dan Sakes关于Volatile用法的总结。 Dan Saks is one of the world's leading experts on the C and C++ programming languages and their use in developing embedded systems. He provides training and ...
volatile关键字是一种类型...操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行 优化,从而可以提供对特殊地址的稳定访问。 使用该关键字的例子如下: int volatile nVint;
详细说明 并举例说明了VOlatile的作用及用法,特别是嵌入式程序员要注意的
主要研究四种变量(属性)的存取速度. volatile nonatomic atomic和正常变量
线程安全、volatile关键字、原子性、并发包、死锁、线程池学习笔记
C语言中关键字volatile的作用,使用说明和例子
关于volatile变量的介绍 11111111111111111111111111
一般对于volatile的解释是这样的:将变量定义为volatile可以防止编译器对变量进行优化,每次均从内存中访问变量,而不是寄存器。既然让编译器优化可以提高访问速度,那为什么又要不用它以及什么时候不用它?其实主要...
1. 过期数据 2. 锁的可见性 4. 原变量 1. JSL 第三版 2. Java Concurrency in Practice
java volatile 关键字实战java volatile 关键字实战java volatile 关键字实战java volatile 关键字实战java volatile 关键字实战java volatile 关键字实战java volatile 关键字实战java volatile 关键字实战java ...
volatile的用法讲解,讲得很详细,希望能帮助到大家
作为指令关键字,volatile的作用是确保本条指令不会因编译器的优化而省略,且要求每次直接读值。下面来一一说明
volatile详解 一、volatile的本意是“易变的” 因为访问寄存器要比访问内存单元快的多,所以编译器一般都会作减少存取内存的优化,但有可能会读脏数据。当要求使用volatile声明变量值的时候,系统总是重新从它所在的...
volatile与synchronized的区别,锁提供了两种主要特性:互斥(mutual exclusion) 和可见性(visibility)
一般说来,volatile用在如下的几个地方: 1、中断服务程序中修改的供其它程序检测的变量需要加volatile; 2、多任务环境下各任务间共享的标志应该加volatile; 3、存储器映射的硬件寄存器通常也要加volatile说明,...
主要讲述java线程volatile关键字