还是说Memory Model,gcc的__sync_synchronize真是太坑爹了!

时间 2012-01-29 03:18:35  IT牛人博客聚合网站
主题 GCC

嗯,还是说可见性的问题。由于CPU和编译器的乱序执行功能,我们经常不得不在代码中手动插入memory barrier。如果你还不清楚memory barrier是什么,那么请先读这个 http://en.wikipedia.org/wiki/Memory_barrier

假如你已经了解它了,那么具体怎么用呢?怎么在代码中插入一个memory barrier ? 用哪个函数?

gcc的手册中有一节叫做”Built-in functions for atomic memory access”,然后里面列举了这样一个函数:

__sync_synchronize (…)

This builtin issues a full memory barrier.

来,我们写段代码试下:

?  C
int main(){
__sync_synchronize();
return 0;
}

然后用gcc4.2编译,

# gcc -S -c test.c

然后看对应的汇编代码,

?  ASM
main:
pushq %rbp
movq %rsp, %rbp
movl $0, %eax
leave
ret

嗯?Nothing at all !!! 不信你试一试,我的编译环境是Freebsd 9.0 release, gcc (GCC) 4.2.1 20070831 patched [FreeBSD]。

好,我换个高版本的gcc编译器试一试,gcc46 (FreeBSD Ports Collection) 4.6.3 20120113 (prerelease)

?  ASM
main:
 
pushq %rbp
 
movq %rsp, %rbp
 
mfence
 
movl $0, %eax
 
popq %rbp
 
ret

看,多了一行,mfence。

怎么回事呢?这是gcc之前的一个BUG:http://gcc.gnu.org/bugzilla/show_bug.cgi?id=36793 。 2008年被发现,然后修复的。其实它之所以是一个BUG,关键在于gcc的开发者很迷惑,mfence在x86 CPU上到底有没有用?有嘛用?

那么mfence到底能不能提供我们想要的结果呢?

Intel的手册中有这么一段我一直没太读懂:

“Processors are free to fetch and cache data speculatively from regions of system memory that use the WB, WC, and WT memory types. This speculative fetching can occur at any time and is not tied to instruction execution. Thus, it is not ordered with respect to executions of the MFENCE instruction; data can be brought into the caches speculatively just before, during, or after the execution of an MFENCE instruction. Processors are free to fetch and cache data speculatively from regions of system memory that use the WB, WC, and WT memory types. This speculative fetching can occur at any time and is not tied to instruction execution. Thus, it is not ordered with respect to executions of the MFENCE instruction; data can be brought into the caches speculatively just before, during, or after the execution of an MFENCE instruction.”

但是在关于Memory Ordering的章节又说:

“Reads cannot pass earlier MFENCE instructions

Writes cannot pass earlier MFENCE instructions.

MFENCE instructions cannot pass earlier reads or writes”

综合起来的意思是,如果代码是这样,

READ A 
MFENCE 
READ B

那么可能会成为这样

READ A 
Speculative READ B 
MFENCE

但是不会成为这样

Speculative READ B 
READ A 
MFENCE

也就是说,Speculative READ可以穿越mfence,但是无法走的更远了,不能再穿越前面的读写的操作。所以无论如何,A、B的读取顺序还是被严格保持的。不知道我的理解对不对。

但是这些只是针对单CPU而言。多CPU(包括超线程和多核)的时候,如果还是用Locked instructions,那么没问题。否则手册里的规则没有特别提到mfence,而是说了这么一句,”Memory ordering obeys causality (memory ordering respects transitive visibility).” Causality 也是一种relaxed Model,我不是很能理解。只看到一句通俗点的解释,” If i see it and tell you about it , then you will see it too.”

这个改动对JAVA社区的影响巨大。JVM通过插入mfence指令,使得volatile变量的写入操作,能得到sequential consistency。于是Java社区现在就只有两条路,要么修改语言规范,要么把mfence换成代价更高的xchg指令。David Dice(transactional memory的TL/TL2算法的发明人)的这篇日志http://blogs.oracle.com/dave/entry/java_memory_model_concerns_on 那写的是相当的悲苦啊。

如果实在想在代码中使用membar,那么最好是认准某个编译器和某个平台。否则,非常推荐intel tbb。tbb:: atomic_fence()。

嗯,最后说下C/C++中的volatile。如果非要去咬文嚼字的看标准,那么这个关键字对于多线程同步简直一点帮助都没有。那么实际呢?我们发现很多人在C/C++中用这个关键字,而且,他们不是在写驱动,就是普通的用户态的代码。他们真的都错了吗?

看看这段神奇的代码:

http://software.intel.com/en-us/articles/single-producer-single-consumer-queue/

这段代码中,所有的membar都可以去掉,无非是告诉编译器,别乱搞。volatile关键字,在具体某个编译器中的含义,要远比ISO标准中说的要多。

这是我今天用tbb写的一个Singleton:

?  CPP
class StatusCodeManager{
public:
static StatusCodeManager& instance(){
if( !value ) {
StatusCodeManager* tmp = new StatusCodeManager();
if( value.compare_and_swap(tmp,NULL)!=NULL )
// Another thread installed the value, so throw away mine.
delete tmp;
}
 
return *value;
}
protected:
StatusCodeManager();
private:
static tbb::atomic<StatusCodeManager*> value;
};

虽然无锁化了,但是并不保证构造函数只执行一次。所以我一直在愁,value->init();插在哪里好呢?

(p.s. 今天找paper的时候看到一篇让我满眼冒金星的神文:《Mathematizing C++ Concurrency》 http://www.cl.cam.ac.uk/~pes20/cpp/popl085ap-sewell.pdf 第一作者是剑桥的某在读博士。)

04-28 05:03