[C++]volatile和原子操作_C/C++_编程开发_程序员俱乐部

中国优秀的程序员网站程序员频道CXYCLUB技术地图
热搜:
更多>>
 
您所在的位置: 程序员俱乐部 > 编程开发 > C/C++ > [C++]volatile和原子操作

[C++]volatile和原子操作

 2016/5/12 5:33:16  aigo  程序员俱乐部  我要评论(0)
  • 摘要:最原始的文章地址已找不到了所谓原子操作,就是"不可中断的一个或一系列操作",在确认一个操作是原子的情况下,多线程环境里面,我们可以避免仅仅为保护这个操作在外围加上性能昂贵的锁,甚至借助于原子操作,我们可以实现互斥锁。很多操作系统都为int类型提供了+-赋值的原子操作版本,比如NT提供了InterlockedExchange等API,Linux/UNIX也提供了atomic_set等函数。前两天有同学问我:在x86上,g_count++(int类型)是否是一个原子操作?我的回答是"不是的
  • 标签:c++ 操作

最原始的文章地址已找不到了

?

所谓原子操作,就是"不可中断的一个或一系列操作" , 在确认一个操作是原子的情况下,多线程环境里面,我们可以避免仅仅为保护这个操作在外围加上性能昂贵的锁,甚至借助于原子操作,我们可以实现互斥锁

很多操作系统都为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 == 五亿 就有了解释,是因为:

  1. CPU对于纯粹的整数运算是很快的,一亿次循环里面,可能只有一两次的线程上下文切换
  2. 没有优化的版本循环体比++操作本身更加耗时,这样切换操作很可能出现在 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的机器就不要这样做了,大家还是加锁吧)

发表评论
用户名: 匿名