volatile、内存屏障、Acquire&Release语义 三者的差别和关系(一) —— 之volatile
2014-03-18 19:53
633 查看
前言:
对于这个题目, 本来想写成一篇博客, 但是写下来发现篇幅有点长, 于是拆分成三篇.
volatile 内存屏障 Acquire&Release语义 这三个概念恐怕是做并行编程的时候, 或者说是做C++多线程编程的过程中很容易搞不明白的概念, 下面依据我的知识范围和认识深度, 做一个不算详细但很认真的解释吧, 最后面再再用LevelDb的原子指针类AtomicPointer举个例子. 如果有不对的地方, 希望得到您的指正.
这是三篇博客的第一篇, 首先讲的是volatile这个关键字.
关于volatile可能很多人的感觉是, 见过, 但是不了解. 有人认为volatile可以用来实现原子操作, 实现线程同步, 首先这是错误的.
在http://en.cppreference.com/上面是这么解释的:
volatile object - an object whose type is volatile-qualified, or a subobject of a volatile object, or a mutable subobject of a const-volatile object. Every access (read or write operation,
member function call, etc.) on the volatile object is treated as a visible side-effect for the purposes of optimization (that is, within a single thread of execution, volatile accesses cannot be reordered or optimized out. This makes volatile objects suitable
for communication with a signal handler, but not with another thread of execution, see std::memory_order)
总结下来可以认为, 在C++中, volatile实现了3个点保证:
( 1 ) 被volatile修饰了的变量的操作不会被编译器优化掉(去除)
( 2 ) 被volatile修饰的变量, 会强制编译器去每次方位这个变量都直接去访问内存对应存储位置(而不是读寄存器或者cpu cache)
( 3 )多个被volatile修饰的变量之间的顺序, 不会被编译器优化调换指令顺序
接下来逐点举例子:
1. volatile修饰了的变量的操作不会被编译器优化掉,看下面的代码, 看看加了volatile和不加有什么区别:
汇编出的结果:
我们可以看到, 编译出来的foo函数里面完全没有a变量的读操作, 也就是说 a; 这行代码被编译器优化掉了, 没了.
我们再看看加上volatile的情况:
编译汇编后:
我们看到把a变量的值读取到eax寄存器的操作了, 虽然这行代码毫无意义, 但是因为加了volatile, 编译器没有把它优化掉, 这就是volatile保证的第1点.
2. volatile修饰的变量, 会强制编译器去每次方位这个变量都直接去访问内存对应存储位置
在现代操作系统中, 从寄存器里面取一个数要比从内存中取快的多, 所以有时候编译器为优化程序, 就会把一些常用变量放到寄存器中, 下次使用该变量的时候就直接从寄存器中取, 而不再访问内存. 这时, 其他线程把内存中的值改变了, 本线程是不可知晓的. 验证代码如下:
上面的代码, 如果wait函数在wake之前开始运行, 我可以先告诉你, wait函数将不会结束(-O2编译). 因为在线程1, 编译器的优化让wait函数只在一开始把flag的内存值读到寄存器一次, 后面一直用寄存器的值来跟count比较, 不再从内存读取flag的值. 看汇编指令验证:
从上面的汇编代码可以看到, flag被读到寄存器edx之后, 当cmp为false则跳到L1结束函数, 为true则跳到L3继续循环. 在这里面我们可以看到, 从第二次cmpl开始汇编指令直接使用edx去做比较, 不再去内存读取flag.
当我们加上volatile呢? volatile int flag = 10; 汇编变成这样子:
汇编结果和没有volatile只有一行的差别, 每次cmpl之前都会重新从内存里面把flag的值读到寄存器edx, 这样, 程序的运行就符合我们的想法了.
完整的测试程序如下(建议实验程序像我这么设计, 如果采用 while(flag==0) 可能这行代码直接被优化掉了, 不能测出volatile的这个第2点保证了
3. 多个被volatile修饰的变量之间的顺序, 不会被编译器优化调换指令顺序
先看下面这个代码:
没有-O2的汇编结果:
加上-O2的汇编结果 (gcc -O2 -S reorder.c && cat reorder.s)
如何解决这个问题呢?
volatile保证的第3点: 多个被volatile修饰的变量之间的顺序, 不会被编译器优化调换指令顺序
实验代码如下:
汇编结果:
到这里可以看到, volatile起作用了. 另外我们会想, 如果只有一个变量加了volatile了呢?
经过实验验证, 只有一个变量加了volatile, 汇编指令的顺序依然被调换, 实验过程不在这里赘述, 留给读者去尝试.
乱序执行是CPU的一种策略, 是为了让流水线技术上, CPU更充满地被使用
流水线是现代RISC核心的一个重要设计,它极大地提高了性能。
对于一条指令的执行过程,通常分为:取指令、指令译码、取操作数、运算、写结果。前面三步由控制器完成,后面两步由运算器完成。按照传统的做法,当控制器工作的时候运算器在休息,在运算器工作的时候控制器在休息。流水线的做法就是当控制器完成第一条指令的操作后,直接开始开始第二条指令的操作,同时运算器开始第一条指令的操作。这样就形成了流水线系统,这是一条2级流水线。
那么, 可以分析出来, 在加了volatile的4行汇编一共需要4个时钟周期完成, 而没有volatile的4行汇编,可以再3个时钟周期完成( "movl B(%rip), %eax" 和 "movl $5, B(%rip)" 可以同时执行 ).
参考资料: http://en.cppreference.com/w/cpp/language/cv http://baiy.cn/doc/cpp/advanced_topic_about_multicore_and_threading.htm
转发请注明出处: http://blog.csdn.net/answer3y/article/details/21476787
对于这个题目, 本来想写成一篇博客, 但是写下来发现篇幅有点长, 于是拆分成三篇.
volatile 内存屏障 Acquire&Release语义 这三个概念恐怕是做并行编程的时候, 或者说是做C++多线程编程的过程中很容易搞不明白的概念, 下面依据我的知识范围和认识深度, 做一个不算详细但很认真的解释吧, 最后面再再用LevelDb的原子指针类AtomicPointer举个例子. 如果有不对的地方, 希望得到您的指正.
这是三篇博客的第一篇, 首先讲的是volatile这个关键字.
关于volatile可能很多人的感觉是, 见过, 但是不了解. 有人认为volatile可以用来实现原子操作, 实现线程同步, 首先这是错误的.
在http://en.cppreference.com/上面是这么解释的:
volatile object - an object whose type is volatile-qualified, or a subobject of a volatile object, or a mutable subobject of a const-volatile object. Every access (read or write operation,
member function call, etc.) on the volatile object is treated as a visible side-effect for the purposes of optimization (that is, within a single thread of execution, volatile accesses cannot be reordered or optimized out. This makes volatile objects suitable
for communication with a signal handler, but not with another thread of execution, see std::memory_order)
总结下来可以认为, 在C++中, volatile实现了3个点保证:
( 1 ) 被volatile修饰了的变量的操作不会被编译器优化掉(去除)
( 2 ) 被volatile修饰的变量, 会强制编译器去每次方位这个变量都直接去访问内存对应存储位置(而不是读寄存器或者cpu cache)
( 3 )多个被volatile修饰的变量之间的顺序, 不会被编译器优化调换指令顺序
接下来逐点举例子:
1. volatile修饰了的变量的操作不会被编译器优化掉,看下面的代码, 看看加了volatile和不加有什么区别:
int a = 1; void foo() { a; }
汇编出的结果:
gcc volatile_test.c -O2 -S && cat volatile_test.s foo: .LFB0: .cfi_startproc rep ret .cfi_endproc
我们可以看到, 编译出来的foo函数里面完全没有a变量的读操作, 也就是说 a; 这行代码被编译器优化掉了, 没了.
我们再看看加上volatile的情况:
volatile int a = 1; void foo() { a; }
编译汇编后:
gcc volatile_test.c -O2 -S && cat volatile_test.s foo: .LFB0: .cfi_startproc movl a(%rip), %eax //从a的内存位置读取值到eax寄存器 ret .cfi_endproc
我们看到把a变量的值读取到eax寄存器的操作了, 虽然这行代码毫无意义, 但是因为加了volatile, 编译器没有把它优化掉, 这就是volatile保证的第1点.
2. volatile修饰的变量, 会强制编译器去每次方位这个变量都直接去访问内存对应存储位置
在现代操作系统中, 从寄存器里面取一个数要比从内存中取快的多, 所以有时候编译器为优化程序, 就会把一些常用变量放到寄存器中, 下次使用该变量的时候就直接从寄存器中取, 而不再访问内存. 这时, 其他线程把内存中的值改变了, 本线程是不可知晓的. 验证代码如下:
//全局定义 int flag=10; //线程1 void wait() { int count = 1; while ( flag != count ) { count = ~count; } } //线程2 void wake() { flag = 1; }
上面的代码, 如果wait函数在wake之前开始运行, 我可以先告诉你, wait函数将不会结束(-O2编译). 因为在线程1, 编译器的优化让wait函数只在一开始把flag的内存值读到寄存器一次, 后面一直用寄存器的值来跟count比较, 不再从内存读取flag的值. 看汇编指令验证:
wait: .LFB0: .cfi_startproc movl flag(%rip), %edx //从内存里读取flag cmpl $1, %edx je .L1 movl $1, %eax .p2align 4,,10 .p2align 3 .L3: notl %eax cmpl %edx, %eax //直接使用edx, 不再去内存里面获取flag的内存值 jne .L3
从上面的汇编代码可以看到, flag被读到寄存器edx之后, 当cmp为false则跳到L1结束函数, 为true则跳到L3继续循环. 在这里面我们可以看到, 从第二次cmpl开始汇编指令直接使用edx去做比较, 不再去内存读取flag.
当我们加上volatile呢? volatile int flag = 10; 汇编变成这样子:
wait: .LFB0: .cfi_startproc movl flag(%rip), %eax cmpl $1, %eax je .L1 movl $1, %eax .p2align 4,,10 .p2align 3 .L3: movl flag(%rip), %edx //再次从内存里面把flag读到寄存器edx notl %eax cmpl %eax, %edx jne .L3
汇编结果和没有volatile只有一行的差别, 每次cmpl之前都会重新从内存里面把flag的值读到寄存器edx, 这样, 程序的运行就符合我们的想法了.
完整的测试程序如下(建议实验程序像我这么设计, 如果采用 while(flag==0) 可能这行代码直接被优化掉了, 不能测出volatile的这个第2点保证了
#include <unistd.h> #include <pthread.h> #include <stdio.h> volatile int flag=10; //这里是否由volatile, 结果不同 void* wait(void* param) { int count = 1; while ( flag != count ) { count = ~count; } printf("wait\n"); } void* wake(void* param) { flag = 1; printf("wake\n"); } int main () { pthread_t t[2]; pthread_create(&t[0], NULL, wait, NULL); sleep(1); pthread_create(&t[1], NULL, wake, NULL); while(1); }
3. 多个被volatile修饰的变量之间的顺序, 不会被编译器优化调换指令顺序
先看下面这个代码:
int A,B; void foo() { A = B+1; B = 5; }
没有-O2的汇编结果:
movl B(%rip), %eax addl $1, %eax //先做加法 movl %eax, A(%rip) movl $5, B(%rip) //再赋值为5
加上-O2的汇编结果 (gcc -O2 -S reorder.c && cat reorder.s)
movl B(%rip), %eax movl $5, B(%rip) //先赋值为5 addl $1, %eax //再执行加法, 顺序调换了 movl %eax, A(%rip)从上面的汇编代码可以看到, 汇编指令的执行顺序, 和原来代码的顺序并不一致.
如何解决这个问题呢?
volatile保证的第3点: 多个被volatile修饰的变量之间的顺序, 不会被编译器优化调换指令顺序
实验代码如下:
volatile int A,B; void foo() { A = B+1; B = 5; }
汇编结果:
$gcc cordering.c -S -O2 && cat cordering.s movl B(%rip), %eax addl $1, %eax //先加1 movl %eax, A(%rip) movl $5, B(%rip) //赋值为5
到这里可以看到, volatile起作用了. 另外我们会想, 如果只有一个变量加了volatile了呢?
经过实验验证, 只有一个变量加了volatile, 汇编指令的顺序依然被调换, 实验过程不在这里赘述, 留给读者去尝试.
乱序执行是CPU的一种策略, 是为了让流水线技术上, CPU更充满地被使用
流水线是现代RISC核心的一个重要设计,它极大地提高了性能。
对于一条指令的执行过程,通常分为:取指令、指令译码、取操作数、运算、写结果。前面三步由控制器完成,后面两步由运算器完成。按照传统的做法,当控制器工作的时候运算器在休息,在运算器工作的时候控制器在休息。流水线的做法就是当控制器完成第一条指令的操作后,直接开始开始第二条指令的操作,同时运算器开始第一条指令的操作。这样就形成了流水线系统,这是一条2级流水线。
那么, 可以分析出来, 在加了volatile的4行汇编一共需要4个时钟周期完成, 而没有volatile的4行汇编,可以再3个时钟周期完成( "movl B(%rip), %eax" 和 "movl $5, B(%rip)" 可以同时执行 ).
参考资料: http://en.cppreference.com/w/cpp/language/cv http://baiy.cn/doc/cpp/advanced_topic_about_multicore_and_threading.htm
转发请注明出处: http://blog.csdn.net/answer3y/article/details/21476787
相关文章推荐
- [置顶] volatile、内存屏障、Acquire&Release语义 三者的差别和关系(二) —— 之内存屏障
- volatile、内存屏障、Acquire&Release语义 三者的差别和关系(二) —— 之内存屏障
- volatile、内存屏障、Acquire&Release语义 三者的差别和关系(一) —— 之volatile
- jvm(二)指令重排 & 内存屏障 & 可见性 & volatile & happen before
- 内存屏障--- asm volatile("" ::: "memory")
- 内存屏障和 volatile 语义
- 内存屏障(__asm__ __volatile__("": : :"memory"))
- 【java多线程系列】java中的volatile的内存语义
- volatile关键字解析&内存模型&并发编程中三概念
- 【java多线程系列】java中的volatile的内存语义
- 电脑结构和CPU、内存、硬盘三者之间的关系
- volatile-内存屏障-互斥锁等-----非常好!
- volatile和锁的内存语义与实现
- Java并发编程系列之四:volatile和锁的内存语义
- 377. Combination Sum IV——DP本质:针对结果的迭代,dp[ans] <= dp[ans-i] & dp[i] 找三者关系 思考问题的维度+1,除了数据集迭代还有考虑结果
- LCM之Fmark功能 && LCD控制器同LCD驱动器的差别 && 帧率与刷新率的关系 && OLED背光
- jvm 内存模型与线程 & Volatile
- volatile内存语义以及实现(一)
- 内存屏障与volatile
- No MFC 编程05 - 进程 > 线程 > 消息队列,三者的包含关系