Memory ordering描述了CPU访问系统内存,执行load和store的顺序。Memory ordering包括编译时编译器生成的和运行时CPU生成的。为了高效地执行指令,只要不影响单线程程序的行为,编译器和CPU常常会对指令进行memory reordering,使得访问内存的操作不会按照程序代码中指定的顺序执行。
在单线程程序中,可以忽略reordering的存在;在多线程程序中,mutex,semaphores等互斥方法会保证在相关函数的调用周围没有reordering。在多核环境下(或对称多处理器架构)下,用C、C++等编写lock-free代码时,memory reordering是可观察到的,是重点要考虑的问题。
Compiler Reordering
从源代码得到CPU指令的过程中,编译器会做很多事情,其中之一就是reordering。(例子摘自Preshing on Programming)
|
|
如果IsPublished = 1;
被reordered到Value = x;
之前,那么IsPublished
作为flag的作用就丧失了。既是是在单线程中,也会有问题。如果该线程在两次store之间被抢占,那么其他使用Value
的线程就会访问到Value
的旧值,而不是新值。
Compiler Barriers
防止编译器reordering最简单的办法莫过于使用compiler barriers,在不想被reordered的两个操作(load和store)之间加入compiler Barrierasm volatile("" ::: "memory")
,但在多核环境下,这并不足够。还需要下面的CPU fence来提供运行时memory barrier的保障。如果使用了CPU fence,那么fence也会作为compiler Barrier。
另一种compiler barrier是函数调用,无论函数是否包含compiler barrier,除了inline函数、声明带有pure
属性的函数和使用了链接时代码生成的函数,大多数函数调用都可以作为compiler barrier。因为编译器根本不知道该函数调用是否会修改先前的值,也不知道这个值是否在函数调用返回后会被继续使用,如果进行了reordering,那么很可能会违反一开始提到的原则。而对于包含的函数,无论是不是inline的,都可以作为compiler barrier。
C/C++中,volatie
会阻止编译器的优化,编译器不会对volatie
变量间的操作进行reordering,但是volatie
对处理器的reordering是无能为力的,并没有happens-before语义。与此同时,volatie
也不能阻止多个线程的并发访问。对于下面的代码,无论shared_data
是不是volatie
,spin_lock
都是必须的。
|
|
代码里的spin_lock
也下面将说的memory barrier。
Processor Reordering
除了编译器会进行reordering,CPU同样会。CPU的reordering仅在多核或多处理器环境下才是可见的。(例子摘自Preshing on Programming)
考虑下面的一段代码,thread1Func
和thread2Func
分别在两个线程中运行,最后r1
和r2
的结果会是什么?为了阻止编译器的reordering,已经在store和load之间加入了compiler barrier。
|
|
由于是并发执行,两个线程的load和store会交替执行,因此r1
和r2
的结果可能为,
|
|
但r1=0 r2=0
也是完全可能的,如果重复的进行测试,那么这个结果会频繁的发生。Intel在64 and IA-32 Architectures Software Developer’s Manual中的8.2.3.4指出,
At each processor, the load and the store are to different locations and hence may be reordered.
代码中每个线程的store和load是不同的内存位置,所以发生StoreLoad Reordering是完全可能的。
Memory Barrier
上述例子的reordering只是众多memory reordering中的一种,主要有四种memory reordering,
- LoadLoad
- StoreStore
- LoadStore
- StoreLoad
类似compiler reordering,需要compiler barrier来阻止CPU的reordering,这里是memory barriers,也叫fence指令。Fence保证了fence之前的load或store和之后的load或store是严格有序的。针对以上四种reordering,有对应的四种barrier,#LoadLoad
、#StoreStore
、#LoadStore
和#StoreLoad
。对于现实中CPU的fence指令的行为,通常是上述几种的融合,且还会有其他的效果。
|
|
需要特别说明的是#StoreLoad
,这个barrier是唯一能够保证上述例子不出现r1=0 r2=0
的。#StoreLoad
保证了在barrier前执行Store1
对其他处理器是可见的,在barrier后执行的Load2
能够得到在barrier之后最新的值(不一定是Store1
的值)。#StoreLoad
防止了后续的load错误的使用Store1
的值,而不是其他处理器在同一内存位置store的更新的值。#StoreLoad
几乎在所有现代的多处理器上都是必须的,同时也是代价最高昂的一种barrier。
除了fence指令,memory barrier还有,
- C++11中,很多原子类型的操作;
- pthread中,mutex,spin_lock,semaphore的操作。