`
aigo
  • 浏览: 2540365 次
  • 性别: Icon_minigender_1
  • 来自: 宜昌
社区版块
存档分类
最新评论

[C++]volatile和原子操作

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 ThreadFuncLPVOID lpParam 
)
{
   
    
int i
;
    
printf"Thread %d start/n", (DWORD*)lpParam 
);
   

    for (i=0<100000000  i++)
        
g_count
++;

    
printf"Thread %d quit/n", (DWORD*)lpParam 
);
    
return 0
;
}

#define THREAD_NUM 
5
VOID mainVOID 
)
{
    
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_countTHREAD_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<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>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的机器就不要这样做了,大家还是加锁吧)

分享到:
评论

相关推荐

    volatile和原子操作有没有关系,我的实验+别人论文

    对volatile的原子性做探究,加上自己的实验代码和实验结果! Case多核?单核?是否有volatile是否编译器优化-O2结果!

    C++ 原子类型

    文章目录atomic构造赋值访问特化操作atomic_flag构造操作内存序 原子对象可以保证:从不同的线程访问其包含的数据不会造成数据竞争。此外,它还能够同步不同线程对内存的访问。 atomic 构造 default (1) atomic()...

    程序员为什么还要刷题-atomique:基于C++11内存模型的.NET的理智原子操作

    目标是用户应该能够编写无锁数据结构和算法,而不必求助于Thread.VolatileRead和Thread.VolatileWrite方法,或Interlocked和Volatile类。 常问问题 为什么是 C++11 内存模型? 简而言之:因为它变得无处不在。 C++11...

    C++大厂面试真题宝典 精选100道完结7章

    volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。 volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的...

    【并发编程】volatile的原理我好像又懂了.pdf

    同步机制:讲解如何使用锁、信号量、原子操作等同步机制来避免竞态条件和死锁。 并发模型:介绍不同的并发模型,如生产者-消费者模型、管道模型、消息传递模型等。 并发工具:介绍并发编程中使用的工具和库,如...

    atomicReference 使用和AtomicStampedReference 解决ABA的问题.docx

    就是俩个线程同事操作,有可能有一个线程已经处理结束,那么第一个线程中间又一次fail操作,会在这个结果上在进行计算 t1 t2 A A-&gt;B-&gt;A A-&gt;C 那我们来看下维基百科上面的形象描述: 你拿着一个装满钱的手提箱在飞机场,...

    CUDA编程指南5.0

    3.1.5 C/C++兼容性 19 3.1.6 64位兼容性 19 3.2 CUDA C运行时 3.2.1 初始化 20 3.2.2 设备存储器 20 3.2.3 共享存储器 24 3.2.4 分页锁定主机存储器 32 3.2.4.1 可分享存储器(portable memory) 34 3.2.4.2 写结合...

    深入理解_Java_虚拟机 JVM_高级特性与最佳实践

    / 316 12.2 硬件的效率与一致性 / 317 12.3 Java内存模型 / 318 12.3.1 主内存与工作内存 / 319 12.3.2 内存间交互操作 / 320 12.3.3 对于volatile型变量的特殊规则 / 322 12.3.4 对于long和double型变量的...

    sesvc.exe 阿萨德

    C++ Python C# Node.Js 一文让你彻底理解 Java HashMap 和 ConcurrentHashMap 2018-07-25 分类:JAVA开发、编程开发、首页精华0人评论 来源:crossoverjie.top 分享到:更多0 前言 Map 这样的 Key Value 在...

    Java 虚拟机面试题全面解析(干货)

    Java与C/C++的编译器对比 物理机如何处理并发问题? Java内存模型 什么是Java内存模型? Java内存模型的目标? 主内存与工作内存 内存间的交互操作 原子性、可见性、有序性 volatile 什么是 volatile? 为什么基于 ...

    汪文君高并发编程实战视频资源全集

    │ 高并发编程第二阶段08讲、并发编程的三个重要概念,原子性,可见性,有序性.mp4 │ 高并发编程第二阶段09讲、指令重排序,happens-before规则精讲.mp4 │ 高并发编程第二阶段10讲、volatile关键字深入详解.mp4...

    汪文君高并发编程实战视频资源下载.txt

    │ 高并发编程第二阶段08讲、并发编程的三个重要概念,原子性,可见性,有序性.mp4 │ 高并发编程第二阶段09讲、指令重排序,happens-before规则精讲.mp4 │ 高并发编程第二阶段10讲、volatile关键字深入详解.mp4...

Global site tag (gtag.js) - Google Analytics