我们知道C++11以后提供了原子操作类型:atomic<T>,该原子模板类可以实现原子操作,比如exchange、compare_exchange_weak、fecth_add等,T类型一般都是非常简单的类型,比如整型、指针等类型,这些类型都可以实现指令级的原子操作,所操作的数据类型的长度一般也不能大于内存数据总线的宽度,比如在32/64bit的处理器中,T类型的数据长度不能超过32/64bit。如果数据类型的长度大于内存数据总线宽度,当读写内存访问这些数据时,就无法使用一条指令对它们进行操作,需要使用多条指令来访问内存,因此在对它们进行操作时,为了保证原子性,只能使用外部的锁加以保护,比如mutex或者自旋锁,显然这种实现机制不是lock-free,既然涉及到锁,进行原子操作时开销比较大。
不过在x86处理器中,因为它的指令集是CISC,可以用一条指令做复杂的操作,它有一条特殊的指令,可以实现对2倍内存数据总线宽度的数据的原子操作,即32/64位的处理器,可以用一条指令来实现64/128位数据的原子操作,下面就以64位的x86处理器来讨论一下。
在X86-64的处理器中,提供了一个可对128bit数据(也就是16字节)进行CAS算法操作的指令:cmpxchg16b(在x86-32处理器中对应的指令是cmpxchg8b)。该指令可以把内存中16字节连续地址的数据进行CAS算法,它的形式是:cmpxchg16b m128,其中m128是一段16字节的连续内存存储位置(即指针)。该指令把rdx:rax寄存器中存放的128位值与内存地址m128中存放的128位值进行比较:如果值相等,则设置零标志(ZF),并将寄存器rcx:rbx存放的值复制到内存位置m128处,否则,ZF标志将被清除,并将内存位置m128处的数据复制到寄存器rdx:rax,可以通过ZF标志来判断操作是否成功。
既然X86处理器有指令可以进行128bit数据的CAS算法,x86环境的C++编译器在编译128bit数据的原子操作函数时,是不是都使用cmpxchg16b指令来实现呢?经过实际分析,发现不同编译器有不同的方案,所产生的汇编代码并不一定使用了cmpxchg16b指令。下面简单地通过atomic<__int128_t>的store()的原子操作来分析一下实现机制。
atomic<__int128_t> am16;
void bar(void)
{
am16.store(42);
}
上面代码片段定义了一个类型为__int128_t,即16字节的atomic对象变量am16,bar()函数调用了am16对象的store()成员函数,分析一下不同编译器生成的汇编代码:
1、编译器GCC,优化选项-O3,所产生的汇编代码如下,没有使用cmpxchg16b指令,而是调用了一个函数:__atomic_store_16:
bar():
mov ecx, 5
mov esi, 42
xor edx, edx
mov edi, OFFSET FLAT:am16
jmp __atomic_store_16
如果运行am16.is_lock_free(),结果返回false,可见GCC所实现的atomic<__int128_t>的原子操作不是lock-free算法。
2、同GCC,编译器CLANG,在-O3优化选项下,生成的汇编代码也没有使用cmpxchg16b指令,也是使用了函数:__atomic_store_16:
bar():
push rax
lea rdi, [rip + am16]
mov esi, 42
xor edx, edx
mov ecx, 5
call __atomic_store_16@PLT
pop rax
ret
如果运行am16.is_lock_free(),结果返回false,可见CLANG实现的atomic<__int128_t>的原子操作也不是lock-free算法。
3、ICC编译器,即Intel C++Compiler编译器,毕竟是Intel自己家的编译器,肯定要为自己家的处理器生成最优的汇编代码了,即使在使用较为低级的-O1优化选项时,它也会产生cmpxchg16b指令:
bar():
..B1.1: # Preds ..B1.