cmpxchg16b指令的实现分析

我们知道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, 5mov     esi, 42xor     edx, edxmov     edi, OFFSET FLAT:am16jmp     __atomic_store_16

如果运行am16.is_lock_free(),结果返回false,可见GCC所实现的atomic<__int128_t>的原子操作不是lock-free算法。

2、同GCC,编译器CLANG,在-O3优化选项下,生成的汇编代码也没有使用cmpxchg16b指令,也是使用了函数:__atomic_store_16:

bar():push    raxlea     rdi, [rip + am16]mov     esi, 42xor     edx, edxmov     ecx, 5call    __atomic_store_16@PLTpop     raxret

如果运行am16.is_lock_free(),结果返回false,可见CLANG实现的atomic<__int128_t>的原子操作也不是lock-free算法。

3、ICC编译器,即Intel C++Compiler编译器,毕竟是Intel自己家的编译器,肯定要为自己家的处理器生成最优的汇编代码了,即使在使用较为低级的-O1优化选项时,它也会产生cmpxchg16b指令:

bar():
..B1.1:                         # Preds ..B1.0push      rbx                                           #549.1mov       esi, offset flat: am16                        #271.2mov       r8d, 42                                       #271.2xor       r9d, r9d                                      #271.2mov       rax, QWORD PTR [rsi]                          #271.2mov       rdx, QWORD PTR [8+rsi]                        #271.2
..L7:                                                           #mov       rbx, r8                                       #271.2mov       rcx, r9                                       #271.2lock      cmpxchg16b QWORD PTR [rsi]                              #271.2jnz       ..L7          # Prob 5%                       #271.2
..B1.2:                         # Preds ..B1.1pop       rbx                                           #551.1ret     

如果运行am16.is_lock_free(),结果返回true,可见ICC为atomic<__int128_t>实现的原子操作是lock-free算法。

那么,如果要操作atomic<__int128_t>的原子操作,在gcc和clang编译环境下,是不是只能使用non-lock-free算法吗?

查看GCC的手册,发现它有一个与此相关的编译选项:-mcx16
This option enables GCC to generate CMPXCHG16B instructions in 64-bit code to implement compare-and-exchange operations on 16-byte aligned 128-bit objects. This is useful for atomic updates of data structures exceeding one machine word in size. (此选项使GCC能够为64位代码生成CMPXCHG16B指令,以在16字节对齐的128位对象上实现比较和交换操作。这对于大小超过一个机器字的数据结构的原子更新非常有用。)

好,那就在编译时加上-mcx16,再分析GCC和CLANG编译器所生成的汇编代码。
1、很遗憾,GCC编译时加上-mcx16,仍然没有生成cmpxchg16b指令代码:

bar():mov     ecx, 5mov     esi, 42xor     edx, edxmov     edi, OFFSET FLAT:am16jmp     __atomic_store_16

2、不过,clang编译时加上-mcx16,生成了cmpxchg16b指令代码:

bar():push    rbxmov     rdx, qword ptr [rip + am16+8]mov     rax, qword ptr [rip + am16]mov     ebx, 42
.LBB0_1:xor     ecx, ecxlock    cmpxchg16b xmmword ptr [rip + am16]jne     .LBB0_1pop     rbxret

如果运行am16.is_lock_free(),返回true,可见在-mcx16编译选项的加持下,CLANG编译器为128bit的原子变量使用了lock-free的原子操作算法。

同样,对于atomic<__int128_t>原子类型的其它原子操作,比如load、exchange、compare_exchange_weak等,各个编译器所生成汇编指令的方案与前面的store操作的方案相同。

由此可见,对于128bit数据的原子类型的原子操作,在ICC编译环境下支持lock-free算法,CLANG编译器只有在使用-mcx16编译选项时,才支持lock-free算法,而GCC编译器无论是否使用-mcx16选项,都不支持lock-free算法。因此,如果我们使用atomic<T>类为128bit的数据实现原子操作时,可能有的编译器并不支持lock-free算法,这样在执行相关原子操作时可能会带来锁的开销。

那么,难道在GCC环境下,要想为128bit的数据实现lock-free算法的原子操作,只能使用汇编语言来手动实现吗?不是的,在GCC编译环境下也能生成cmpxchg16b的指令,只不过不是使用atomic模板类,而是使用它的内置函数:bool __sync_bool_compare_and_swap(type *ptr, type oldval, type newval);
该函数是GCC提供的内置函数,用于实现CAS操作的原子性,它可以和编译选项-mcx16结合来实现128bit数据的CAS原子操作。它会比较指针ptr指向的值与给定的旧值oldval,如果相同,则将新值newval写入,并返回真(true);如果不同,则不写入,并返回假(false)。
下面看一下使用它如何来实现__int128_t类型的store原子写操作:

__int128_t atomic_val;
void store() {__int128_t val = 42;__int128_t cmp;do {cmp = atomic_val;} while (!__sync_bool_compare_and_swap(&atomic_val, cmp, val));
}

下面是GCC使用-mcx16编译选项产生的汇编代码,此时编译器使用了cmpxchg16b指令,使用lock-free算法实现了原子赋值操作。

store():push    rbxxor     ecx, ecxmov     ebx, 42
.L20:mov     rax, QWORD PTR global[rip]mov     rdx, QWORD PTR global[rip+8]lock 	cmpxchg16b XMMWORD PTR global[rip]jne     .L20pop     rbxret

同样,编译器ICC以及CLANG在使用-mcx16编译选项时,也生成了类似的lock-free算法的原子赋值操作。

不过,需要说明的是,因为bool __sync_bool_compare_and_swap(__int128_t *ptr, __int128_t cmp, __int128_t val)是C函数,它的第二个参数cmp是值类型,当CAS运算失败时无法同时把指针ptr指向的原值atomic_val通过cmp参数返回。因此,在CAS失败后循环重试时,需要再次通过cmp = atomic_val;把原值赋值给cmp值。而指令cmpxchg16b在失败时,会把原子变量的原值通过rdx:rax返回,因此,失败后循环重试时第6、7行代码,其实寄存器rdx:rax中所存放的值和寄存器rdi所指向位置的128bit的值完全相同,没必要再为rdx:rax做一次赋值操作,而atomic的store操作就没有这样的冗余操作。可见__sync_bool_compare_and_swap()生成的store原子操作的汇编代码没有前面的am.store()生成的汇编代码高效。

此外,指令cmpxchg16b都使用了前缀lock,以保证CAS操作是原子的,在X86处理器环境中,lock指令前缀还能保证sequence-consistency的内存序,也就是说store()等同于atomic.store(42, memory_order_seq_cst)的操作。

最后,就用cmpxchg16b的CAS算法来实现一个128bit的加法原子操作,128bit的数据是由两个64bit的数据组成,假设它们是两个计数器,我们使用多线程同时分别为这两个进行累加操作。因为atomic<__int128_t>类型的原子类,不支持fetch_add等算术运算,而compared_exchange_weak等CAS函数在GCC编译器中又不是lock-free算法实现,基于前面的分析,不建议直接使用原子类来实现,可以使用__sync_bool_compare_and_swap()内置函数来实现功能。

void atomic_add(__int128_t &data, int64_t x, int64_t y) {__int128_t val;__int128_t cmp;do {cmp = data;val = cmp;int64_t *p = (int64_t *) &val;p[0] += x;p[1] += y;} while (!__sync_bool_compare_and_swap(&data, cmp, val));
}#define NUM 10000000
int main(void)
{__int128_t m16 = 0;int64_t *p = (int64_t *) &m16;printf("init: %d-%d\n", *p++, *p);auto lamb = [&m16]() {for (volatile int i=0; i<NUM; i++) {atomic_add(m16, 1, 1);}};thread t1(lamb);thread t2(lamb);thread t3(lamb);thread t4(lamb);t1.join();t2.join();t3.join();t4.join();p = (int64_t *) &m16;printf("finish: %d-%d\n", *p++, *p);
}

分别使用GCC、CLANG加上-mcx16和ICC编译,它们生成的程序运行结束后,输出结果都是:

init: 0-0
finish: 40000000-40000000

组成128bit变量的两个int64_t分量,它们分别被连续累加1,进行了1千万次,共4个线程,因此最终结果是4千万,结果运行正确。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/7137.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

刷题总结 回溯算法

为了方便复习并且在把算法忘掉的时候能尽量快速的捡起来 刷完回溯算法这里需要做个总结 回溯算法的适用范围 回溯算法是深度优先搜索&#xff08;DFS&#xff09;的一种特定应用&#xff0c;在DFS的基础上引入了约束检查和回退机制。 相比于普通的DFS&#xff0c;回溯法的优…

【MySQL】我在广州学Mysql 系列——MySQL用户管理详解

ℹ️大家好&#xff0c;我是练小杰&#xff0c;本博客是春节前最后一篇了&#xff0c;在此感谢大佬们今年的支持&#xff01;&#xff01;&#x1f64f;&#x1f64f; 接下来将学习MYSQL用户管理的相关概念以及命令~~ 回顾&#xff1a;&#x1f449;【MYSQL触发器的使用】 数据…

网络编程-网络原理HTTP1

文章目录 HTTP请求/响应的基本结构认识URLURL是什么和基本格式关于encoding机制 认识方法(method)GET方法简介GET方法的特点POST方法简介POST方法的特点GET和POST的区别(经典面试题)关于GET和POST的补充说明Restful风格 上节主要是对http协议的一些最基本的概念做出一些说明, 然…

概率密度函数(PDF)分布函数(CDF)——直方图累积直方图——直方图规定化的数学基础

对于连续型随机变量&#xff0c;分布函数&#xff08;Cumulative Distribution Function, CDF&#xff09;是概率密度函数&#xff08;Probability Density Function, PDF&#xff09;的变上限积分&#xff0c;概率密度函数是分布函数的导函数。 如果我们有一个连续型随机变量…

[Python学习日记-79] socket 开发中的粘包现象(解决模拟 SSH 远程执行命令代码中的粘包问题)

[Python学习日记-79] socket 开发中的粘包现象&#xff08;解决模拟 SSH 远程执行命令代码中的粘包问题&#xff09; 简介 粘包问题底层原理分析 粘包问题的解决 简介 在Python学习日记-78我们留下了两个问题&#xff0c;一个是服务器端 send() 中使用加号的问题&#xff0c…

【落羽的落羽 数据结构篇】算法复杂度

文章目录 一、数据结构和算法简介二、算法复杂度1. 时间复杂度2. 空间复杂度 一、数据结构和算法简介 数据结构是计算机存储、组织数据的方式&#xff0c;指相互之间存在一种或多种特定关系的数据元素的集合。没有一种单一的数据结构对所有用途都有用&#xff0c;所以我们要学…

22_解析XML配置文件_List列表

解析XML文件 需要先 1.【加载XML文件】 而 【加载XML】文件有两种方式 【第一种 —— 使用Unity资源系统加载文件】 TextAsset xml Resources.Load<TextAsset>(filePath); XmlDocument doc new XmlDocument(); doc.LoadXml(xml.text); 【第二种 —— 在C#文件IO…

第十五届蓝桥杯大赛软件赛省赛C/C++ 大学 B 组

第十五届的题目在规定时间内做出了前5道&#xff0c;还有2道找时间再磨一磨。现在把做的一些思路总结如下&#xff1a; 题1&#xff1a;握手问题 问题描述 小蓝组织了一场算法交流会议&#xff0c;总共有 50人参加了本次会议。在会议上&#xff0c;大家进行了握手交流。按照惯例…

联想电脑怎么设置u盘启动_联想电脑设置u盘启动方法(支持新旧机型)

有很多网友问联想电脑怎么设置u盘启动&#xff0c;联想电脑设置u盘启动的方法有两种&#xff0c;一是通过bios进行设置。二是通过快捷方式启动进入u盘启动。但需要注意有两种引导模式是&#xff0c;一种是uefi引导&#xff0c;一种是传统的leacy引导&#xff0c;所以需要注意制…

GitHub Actions 使用需谨慎:深度剖析其痛点与替代方案

在持续集成与持续部署&#xff08;CI/CD&#xff09;领域&#xff0c;GitHub Actions 曾是众多开发者的热门选择&#xff0c;但如今&#xff0c;其弊端逐渐显现&#xff0c;让不少人在使用前不得不深思熟虑。 团队由大约 15 名工程师组成&#xff0c;采用基于主干的开发方式&am…

Leetcode-两数相加

给你两个 非空 的链表&#xff0c;表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的&#xff0c;并且每个节点只能存储 一位 数字。 请你将两个数相加&#xff0c;并以相同形式返回一个表示和的链表。 你可以假设除了数字 0 之外&#xff0c;这两个数都不会以 0 …

MySQL安装教程

一、下载 点开下面的链接&#xff1a;下载地址 点击Download 就可以下载对应的安装包了, 安装包如下: 二、解压 下载完成后我们得到的是一个压缩包&#xff0c;将其解压&#xff0c;我们就可以得到MySQL 8.0.34 的软件本体了(就是一个文件夹)&#xff0c;我们可以把它放在你想…

BGP分解实验·11——路由聚合与条件性通告(3)

续接上&#xff08;2&#xff09;的实验。其拓扑如下&#xff1a; 路由聚合的负向也就是拆分&#xff0c;在有双出口的情况下&#xff0c;在多出口做流量分担是优选方法之一。 BGP可以根据指定来源而聚合路由&#xff0c;在产生该聚合路由的范围内的条目注入到本地BGP表后再向…

INCOSE需求编写指南-第1部分:介绍

第1部分&#xff1a;介绍Section 1: Introduction 1.1 目的和范围 Purpose and Scope 本指南专门介绍如何在系统工程背景下以文本形式表达需求和要求陈述。其目的是将现有标准&#xff08;如 ISO/IEC/IEEE 29148&#xff09;中的建议以及作者、主要贡献者和审稿员的最佳实践结…

基于神经网络的视频编码NNVC(1):帧内预测

在H.266/VVC发布后&#xff0c;基于传统编码框架提升压缩率越来越难&#xff0c;随着深度学习的发展&#xff0c;研究人员开始尝试将神经网络引入编码器。为此&#xff0c;JVET工作组在2020年成立AHG11小组来专门进行基于神经网络的视频编码的研究。 为了方便研究&#xff0c;工…

深入探究分布式日志系统 Graylog:架构、部署与优化

文章目录 一、Graylog简介二、Graylog原理架构三、日志系统对比四、Graylog部署传统部署MongoDB部署OS或者ES部署Garylog部署容器化部署 五、配置详情六、优化网络和 REST APIMongoDB 七、升级八、监控九、常见问题及处理 一、Graylog简介 Graylog是一个简单易用、功能较全面的…

寒假1.23

题解 web&#xff1a;[极客大挑战 2019]Secret File&#xff08;文件包含漏洞&#xff09; 打开链接是一个普通的文字界面 查看一下源代码 发现一个链接&#xff0c;点进去看看 再点一次看看&#xff0c;没什么用 仔细看&#xff0c;有一个问题&#xff0c;当点击./action.ph…

ORB-SLAM2源码学习:Initializer.cc⑧: Initializer::CheckRT检验三角化结果

前言 ORB-SLAM2源码学习&#xff1a;Initializer.cc⑦: Initializer::Triangulate特征点对的三角化_cv::svd::compute-CSDN博客 经过上面的三角化我们成功得到了三维点&#xff0c;但是经过三角化成功的三维点并不一定是有效的&#xff0c;需要筛选才能作为初始化地图点。 …

微信小程序1.1 微信小程序介绍

1.1 微信小程序介绍 内容提要 1.1 什么是微信小程序 1.2 微信小程序的功能 1.3 微信小程序使用场景 1.4 微信小程序能取代App吗 1.5 微信小程序的发展历程 1.6微信小程序带来的机会

STM32 GPIO配置 点亮LED灯

本次是基于STM32F407ZET6做一个GPIO配置&#xff0c;实现点灯实验。 新建文件 LED.c、LED.h文件&#xff0c;将其封装到Driver文件中。 双击Driver文件将LED.c添加进来 编写头文件&#xff0c;这里注意需要将Driver头文件声明一下。 在LED.c、main.c里面引入头文件LED.h LED初…