和我一起学习leveldb [1 port]
#[原子操作]
port目录是用来做平台相关工作的,这个目录里面的代码比较简单。既然简单,那么我就可以多用一些篇幅来讨论细节问题,力争能够深入浅出,把技术问题讨论清楚。
这个目录里面的代码结构如下:
port
|
+--- port.h <=== 对外提供的头文件,根据不同平台include不同的头文件,无需分析
|
+--- atomic_pointer.h <=== 支持原子操作的指针
|
+--- port_posix.h <=== posix平台
+--- port_posix.cc
|
+--- port_android.h <=== android平台
+--- port_android.cc
|
+--- port_example.h <=== 为支持leveldb在其他平台提供的一个实现接口的例子
|
+--- win
|
+--- stdint.h <=== windows平台(关于windows平台的知识我不懂,因此不分析)
我们首先来看atomic_pointer.h。这里面提供了一个AtomicPointer类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// AtomicPointer built using platform-specific MemoryBarrier()
#if defined(LEVELDB_HAVE_MEMORY_BARRIER)
class AtomicPointer {
private:
void* rep_;
public:
AtomicPointer() { }
explicit AtomicPointer(void* p) : rep_(p) {}
inline void* NoBarrier_Load() const { return rep_; }
inline void NoBarrier_Store(void* v) { rep_ = v; }
inline void* Acquire_Load() const {
void* result = rep_;
MemoryBarrier();
return result;
}
inline void Release_Store(void* v) {
MemoryBarrier();
rep_ = v;
}
};
AtomicPointer提供了四种操作:
void* AtomicPointer::NoBarrier_Load() --- 非Barrier得到存入的指针
void AtomicPointer::NoBarrier_Store(void* v) --- 非Barrier存入指针
void* AtomicPointer::Acquire_Load() --- Barrier获取指针
void AtomicPointer::Release_Store(void *v) --- Barrier存入指针
我们先看在宏定义LEVELDB_HAVE_MEMORY_BARRIER的这一段代码,在前面两个NoBarrier前缀的函数中,其实就是简单地做了一件事,用成员变量保存一个指针或者是把保存的指针返回,而后面两个函数的最后一句话前面,都有一个MemoryBarrier()
调用。那MemoryBarrier的作用是什么呢?其实它是用于防止编译器优化。我们知道,如果是几个不相关的指令,或者是可以优化的指令,例如
1
2
3
4
// example 1
int a = 1;
char *b = "hello world";
short c = 3;
或
1
2
3
4
5
6
// example 2
// void *ptr, v, _store;
v = ptr;
_store = v;
somefunc();
v = _store;
编译器可能会处于效率方面的考虑,更改前面a,b,c几个指令的执行顺序。在第二段示例代码中,v的值是没有改变的,那么编译器可能会认为_store = v; v = _store;
是多余的,就直接把这一段给“优化”掉了。这段代码在单线程中确实是多余的,但是在多线程环境下,可能在somefunc()
被调用的时候,另一个线程把v的值给改变了,而这种情况是编译器无法发现的。因此,为了避免编译器优化,就在这里加一个MemoryBarrier指令,这样就相当于告诉编译器,在MemoryBarrier前后的代码可能因为某种原因发生了改变,优化的时候,绝对不能把MemoryBarrier后面的代码提到前面去,也不能把优化掉。我再举一个例子:
1
2
3
4
5
// get start time
for (int i = 0; i != 100000; i++) {
MemoryBarrier()
}
// get end time
这段代码,是想知道for循环空转100000次的耗时,这里就需要加入一个MemoryBarrier,如果不加,那么编译器可能就会直接把这个无意义的for循环直接优化掉了。
那么MemoryBarrier到底是怎么实现的呢?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// Define MemoryBarrier() if available
// Windows on x86
#if defined(OS_WIN) && defined(COMPILER_MSVC) && defined(ARCH_CPU_X86_FAMILY)
// windows.h already provides a MemoryBarrier(void) macro
// http://msdn.microsoft.com/en-us/library/ms684208(v=vs.85).aspx
#define LEVELDB_HAVE_MEMORY_BARRIER
// Gcc on x86
#elif defined(ARCH_CPU_X86_FAMILY) && defined(__GNUC__)
inline void MemoryBarrier() {
// See http://gcc.gnu.org/ml/gcc/2003-04/msg01180.html for a discussion on
// this idiom. Also see http://en.wikipedia.org/wiki/Memory_ordering.
__asm__ __volatile__("" : : : "memory");
}
#define LEVELDB_HAVE_MEMORY_BARRIER
// Sun Studio
#elif defined(ARCH_CPU_X86_FAMILY) && defined(__SUNPRO_CC)
inline void MemoryBarrier() {
// See http://gcc.gnu.org/ml/gcc/2003-04/msg01180.html for a discussion on
// this idiom. Also see http://en.wikipedia.org/wiki/Memory_ordering.
asm volatile("" : : : "memory");
}
#define LEVELDB_HAVE_MEMORY_BARRIER
// Mac OS
#elif defined(OS_MACOSX)
inline void MemoryBarrier() {
OSMemoryBarrier();
}
#define LEVELDB_HAVE_MEMORY_BARRIER
// ARM
#elif defined(ARCH_CPU_ARM_FAMILY)
typedef void (*LinuxKernelMemoryBarrierFunc)(void);
LinuxKernelMemoryBarrierFunc pLinuxKernelMemoryBarrier __attribute__((weak)) =
(LinuxKernelMemoryBarrierFunc) 0xffff0fa0;
inline void MemoryBarrier() {
pLinuxKernelMemoryBarrier();
}
#define LEVELDB_HAVE_MEMORY_BARRIER
#endif
从这一些宏定义我们可以看到,在X86环境下,直接用一句内嵌汇编指令 __asm__ __volatile__("" : : : "memory");
就可以实现内存屏障。MacOS自带了内存屏障调用,请在MacOS下面man atomic
查看,比较神奇的是ARM,它直接提供了一个内存地址(函数指针),指向内存屏障调用,关于这个请参考:这里。另外,我再额外补充一下,GCC也提供一系列原子操作和内存屏障函数,例如__sync_synchronize()
另外说明一下,有些同学可能会有疑问,Acquire_Load()
和 Release_Store()
是两个函数调用,那里里面的指令怎么会跟上下文产生关系而可能被编译器优化呢?需要注意的是,这几个函数都是inline函数,编译器会根据具体情况,把inline编译成函数调用或者直接进行宏替换。因此,在C++编程中,千万不能把一个inline函数的地址传递给一个函数指针,或者是通过指向类的指针去调用inline函数,这些做法都有可能导致不可预知的错误(笔者以前有血的教训啊,程序莫名其妙的崩在一个函数调用上,最后发现调用的是一个inline函数,可能由于编译器优化的原因,inline函数被编译器替换成了宏,遇到导致调用了一个非法的地址)。
接下来看另外一部分宏定义,如果系统不支持 MemoryBarrier,那么就去看是否有cstdatomic库,用cstdatomic来实现内存屏障。如果cstdatomic库也没有,那么很抱歉,编译失败,你得自己去为你的平台实现一套原子操作,port_example.h告诉了你该去为你的平台实现些什么接口。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// AtomicPointer based on <cstdatomic>
#elif defined(LEVELDB_CSTDATOMIC_PRESENT)
class AtomicPointer {
private:
std::atomic<void*> rep_;
public:
AtomicPointer() { }
explicit AtomicPointer(void* v) : rep_(v) { }
inline void* Acquire_Load() const {
return rep_.load(std::memory_order_acquire);
}
inline void Release_Store(void* v) {
rep_.store(v, std::memory_order_release);
}
inline void* NoBarrier_Load() const {
return rep_.load(std::memory_order_relaxed);
}
inline void NoBarrier_Store(void* v) {
rep_.store(v, std::memory_order_relaxed);
}
};
// We have neither MemoryBarrier(), nor <cstdatomic>
#else
#error Please implement AtomicPointer for this platform.
最后提一下,注意AtomicPointer的单参构造函数 AtomicPointer(void *v)
前面加了explicit关键字,这是C++程序员必须具备的基本素养之一。如果你还不知道explicit是干什么用了,请自行google之。leveldb的代码注释详尽,C++使用相当规范,仔细阅读可以从里面学到很多有用的C++编程技能。