热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

x86_64上的原子双浮点或SSE/AVX向量加载/存储

如何解决《x86_64上的原子双浮点或SSE/AVX向量加载/存储》经验,为你挑选了2个好方法。

在这里(以及一些SO问题)我看到C++不支持像无锁的东西,std::atomic并且还不能支持像原子AVX/SSE向量这样的东西,因为它依赖于CPU(虽然现在我知道CPU,ARM, AArch64和x86_64有矢量).

但是double在x86_64中对s或向量的原子操作是否有汇编级支持?如果是这样,支持哪些操作(如加载,存储,添加,减去,可能相乘)?MSVC++ 2017实现哪些操作无锁atomic



1> Peter Cordes..:

C++不支持无锁等功能 std::atomic

实际上,C++ 11 std::atomic在典型的C++实现上是无锁的,并且确实暴露了几乎所有你可以在asm中用float/无doublex86 进行无锁编程的事情(例如,加载,存储和CAS足以实现任何东西:为什么不是'原子双完全实现).但是,当前的编译器并不总能atomic有效地编译.

C++ 11 std :: atomic没有用于Intel的事务内存扩展(TSX)的API (用于FP或整数).TSX可能会改变游戏规则,尤其是FP/SIMD,因为它可以消除xmm和整数寄存器之间弹跳数据的所有开销.如果事务没有中止,那么你用双重或向量加载/存储做的任何事情都会以原子方式发生.

一些非x86硬件支持float/double的原子添加,而C++ p0020是一个向C++的/ 添加fetch_addoperator+=/ -=template特化的提议. std::atomic

具有LL/SC原子而不是x86样式的内存目的地指令的硬件,例如ARM和大多数其他RISC CPU,可以在有doublefloat没有CAS的情况下进行原子RMW操作,但是你仍然必须从FP到整数寄存器获取数据,因为LL/SC通常仅适用于整数寄存器,如x86 cmpxchg.但是,如果硬件仲裁LL/SC对以避免/减少活锁,那么在非常高争用的情况下,它将比CAS循环更有效.如果您设计的算法因此争用很少,那么fetch_add的LL/add/SC重试循环与load + add + LL/SC CAS重试循环之间可能只有很小的代码大小差异.


x86自然对齐的加载和存储最多可达8个字节,甚至是x87或SSE.(例如movsd xmm0, [some_variable],即使在32位模式下也是原子的).事实上,gcc使用x87 fild/ fistp或SSE 8B加载/存储来实现std::atomic32位代码的加载和存储.

具有讽刺意味的是,编译器(gcc7.1,clang4.0,ICC17,MSVC CL19)在64位代码(或32位SSE2可用)中表现不佳,并通过整数寄存器反弹数据而不是直接进行movsd加载/存储往返于xmm regs(在Godbolt上看到它):

#include 
std::atomic ad;

void store(double x){
    ad.store(x, std::memory_order_release);
}
//  gcc7.1 -O3 -mtune=intel:
//    movq    rax, xmm0               # ALU xmm->integer
//    mov     QWORD PTR ad[rip], rax
//    ret

double load(){
    return ad.load(std::memory_order_acquire);
}
//    mov     rax, QWORD PTR ad[rip]
//    movq    xmm0, rax
//    ret

没有-mtune=intel,gcc喜欢存储/重载整数 - > xmm.请参阅https://gcc.gnu.org/bugzilla/show_bug.cgi?id=80820以及我报告的相关错误.这甚至是一个糟糕的选择-mtune=generic.AMD movq在整数和向量寄存器之间具有高延迟,但它也具有存储/重载的高延迟.使用默认值-mtune=generic,load()编译为:

//    mov     rax, QWORD PTR ad[rip]
//    mov     QWORD PTR [rsp-8], rax   # store/reload integer->xmm
//    movsd   xmm0, QWORD PTR [rsp-8]
//    ret

在xmm和整数寄存器之间移动数据将带我们进入下一个主题:


原子读 - 修改 - 写(类似fetch_add)是另一个故事:直接支持整数与类似的东西lock xadd [mem], eax(有关更多详细信息,请参阅可以使用num ++为'int num'的原子).对于其他东西,比如atomicatomic,x86上的唯一选项是带cmpxchg(或TSX)的重试循环.

原子比较和交换(CAS)可用作任何原子RMW操作的无锁构建块,最大硬件支持的CAS宽度.在x86-64上,这是16字节cmpxchg16b(在某些第一代AMD K8上不可用,所以对于gcc你必须使用-mcx16-march=whatever启用它).

gcc使得最好的asm成为可能exchange():

double exchange(double x) {
    return ad.exchange(x); // seq_cst
}
    movq    rax, xmm0
    xchg    rax, QWORD PTR ad[rip]
    movq    xmm0, rax
    ret
  // in 32-bit code, compiles to a cmpxchg8b retry loop


void atomic_add1() {
    // ad += 1.0;           // not supported
    // ad.fetch_or(-0.0);   // not supported
    // have to implement the CAS loop ourselves:

    double desired, expected = ad.load(std::memory_order_relaxed);
    do {
        desired = expected + 1.0;
    } while( !ad.compare_exchange_weak(expected, desired) );  // seq_cst
}

    mov     rax, QWORD PTR ad[rip]
    movsd   xmm1, QWORD PTR .LC0[rip]
    mov     QWORD PTR [rsp-8], rax    # useless store
    movq    xmm0, rax
    mov     rax, QWORD PTR [rsp-8]    # and reload
.L8:
    addsd   xmm0, xmm1
    movq    rdx, xmm0
    lock cmpxchg    QWORD PTR ad[rip], rdx
    je      .L5
    mov     QWORD PTR [rsp-8], rax
    movsd   xmm0, QWORD PTR [rsp-8]
    jmp     .L8
.L5:
    ret

compare_exchange总是进行逐位比较,所以你不必担心负零(-0.0)+0.0在IEEE语义中比较等于或者NaN是无序的.如果您尝试检查desired == expected并跳过CAS操作,这可能是一个问题.对于足够新的编译器,memcmp(&expected, &desired, sizeof(double)) == 0可能是表达C++中FP值的按位比较的好方法.只要确保你避免误报; 假阴性只会导致不需要的CAS.


硬件仲裁lock or [mem], 1肯定比在lock cmpxchg重试循环中旋转多个线程更好.每次核心访问高速缓存行但失败时,cmpxchg与整数内存目标操作相比,浪费吞吐量,一旦他们获得高速缓存行,它们总是成功.

IEEE浮点数的一些特殊情况可以使用整数运算来实现.例如,a的绝对值atomic可以用lock and [mem], rax(其中RAX具有除符号位设置之外的所有位).或者通过将1加入符号位来强制浮点/双精度为负.或者用XOR切换其标志.你甚至可以原子地将它的大小增加1 ulp lock add [mem], 1.(但是,只有当你确定它不是无限的时候才开始... nextafter()是一个有趣的功能,这要归功于具有偏向指数的IEEE754的非常酷的设计,这使得从尾数到指数的实际运行工作.)

可能没有办法在C++中表达这一点,让编译器在使用IEEE FP的目标上为你做这件事.因此,如果你想要它,你可能必须自己使用类型惩罚atomic或其他东西,并检查FP字节顺序是否匹配整数字节序等等.(或者只是为x86做它.大多数其他目标有LL/SC而不是内存目的地锁定操作.)


还不能支持原子AVX/SSE向量之类的东西,因为它依赖于CPU

正确.通过缓存一致性系统,无法检测128b或256b存储或加载何时是原子的.(https://gcc.gnu.org/bugzilla/show_bug.cgi?id=70490).甚至在L1D和执行单元之间具有原子传输的系统也可能在通过窄协议在高速缓存之间传输高速缓存行时在8B块之间撕裂.真实示例:具有HyperTransport互连的多插槽Opteron K10似乎在单个插槽中具有原子16B加载/存储,但不同插槽上的线程可以观察到撕裂.

但是如果你有一个共享的对齐doubles 数组,你应该可以在它们上面使用向量加载/存储,而不会在任何给定的内部"撕裂" double.

向量加载/存储和收集/分散的每元素原子性?

我认为可以安全地假设对齐的32B加载/存储是通过不重叠的8B或更宽的加载/存储完成的,尽管英特尔不保证这一点.对于未对齐的操作,假设任何东西可能都不安全.

如果你需要一个16B原子的负载下,唯一的选择是lock cmpxchg16b,用desired=expected.如果成功,它会将现有值替换为自身.如果失败,那么你得到旧的内容.(转角情况:只读内存上的这个"加载"错误,所以要小心你传递给执行此操作的函数的指针.)此外,与实际的只读负载相比,性能当然是可怕的缓存线处于共享状态,并且不是完全内存屏障.

16B原子商店和RMW都可以使用lock cmpxchg16b明显的方式.这使得纯存储比常规矢量存储更昂贵,特别是如果cmpxchg16b必须重试多次,但原子RMW已经很昂贵.

将矢量数据移入/移出整数寄存器的额外指令不是免费的,但与之相比并不昂贵lock cmpxchg16b.

# xmm0 -> rdx:rax, using SSE4
movq   rax, xmm0
pextrq rdx, xmm0, 1


# rdx:rax -> xmm0, again using SSE4
movq   xmm0, rax
pinsrq xmm0, rdx, 1

在C++ 11术语中:

atomic<__m128d>即使对于只读或只写操作(使用cmpxchg16b),即使以最佳方式实现,也会很慢. atomic<__m256d>甚至无法锁定.

alignas(64) atomic shared_buffer[1024];在理论上仍然允许自动向量化的代码读取或写入它,只需要到movq rax, xmm0,然后xchgcmpxchg用于原子RMW上double.(在32位模式下,cmpxchg8b可以工作.)但是你几乎肯定不会从编译器中获得好的asm,但是!


您可以自动更新16B对象,但可以原子方式分别读取8B半部分.(我认为这对于x86上的内存排序是安全的:请参阅https://gcc.gnu.org/bugzilla/show_bug.cgi?id=80835上的推理).

但是,编译器没有提供任何干净的方式来表达这一点.我修改了一个适用于gcc/clang的联合类型 - 惩罚:我如何用c ++ 11 CAS实现ABA计数器?.但gcc7及更高版本不会内联cmpxchg16b,因为他们正在重新考虑16B对象是否应该真正表现为"无锁".(https://gcc.gnu.org/ml/gcc-patches/2017-01/msg02344.html).



2> avdgrinten..:

在x86-64上,原子操作通过LOCK前缀实现.在英特尔软件开发者手册(第2卷,指令集)的状态

LOCK前缀只能作为以下指令的前缀,并且只能作为目标操作数是存储器操作数的那些形式的指令:ADD,ADC,AND,BTC,BTR,BTS,CMPXCHG,CMPXCH8B,CMPXCHG16B,DEC,INC, NEG,NOT,OR,SBB,SUB,XOR,XADD和XCHG.

这些指令都不对浮点寄存器(如XMM,YMM或FPU寄存器)进行操作.

这意味着在x86-64上实现原子浮点/双精度操作没有自然的方法.虽然大多数这些操作可以通过将浮点值的位表示加载到通用(即整数)寄存器来实现,但这样做会严重降低性能,因此编译器作者选择不实现它.

正如Peter Cordes在评论中指出的那样,加载和存储不需要LOCK前缀,因为它们在x86-64上始终是原子的.但是,英特尔SDM(第3卷,系统编程指南)仅保证以下加载/存储是原子的:

读取或写入单个字节的指令.

读取或写入地址在2字节边界上对齐的字(2个字节)的指令.

读取或写入双字(4字节)的指令,其地址在4字节边界上对齐.

读取或写入四字(8字节)的指令,其地址在8字节边界上对齐.

特别是,不保证从较大的XMM和YMM向量寄存器加载/存储的原子性.


有一些指令`cmpxchg8b`,`cmpxchg16b`允许CASsing 64/128位,从而允许对双精度/ SSE进行通用原子操作.此外,RMW指令不一定比加载/操作/存储序列更快.
推荐阅读
  • 本文由编程笔记#小编整理,主要介绍了关于数论相关的知识,包括数论的算法和百度百科的链接。文章还介绍了欧几里得算法、辗转相除法、gcd、lcm和扩展欧几里得算法的使用方法。此外,文章还提到了数论在求解不定方程、模线性方程和乘法逆元方面的应用。摘要长度:184字。 ... [详细]
  • 本文介绍了C#中生成随机数的三种方法,并分析了其中存在的问题。首先介绍了使用Random类生成随机数的默认方法,但在高并发情况下可能会出现重复的情况。接着通过循环生成了一系列随机数,进一步突显了这个问题。文章指出,随机数生成在任何编程语言中都是必备的功能,但Random类生成的随机数并不可靠。最后,提出了需要寻找其他可靠的随机数生成方法的建议。 ... [详细]
  • Android Studio Bumblebee | 2021.1.1(大黄蜂版本使用介绍)
    本文介绍了Android Studio Bumblebee | 2021.1.1(大黄蜂版本)的使用方法和相关知识,包括Gradle的介绍、设备管理器的配置、无线调试、新版本问题等内容。同时还提供了更新版本的下载地址和启动页面截图。 ... [详细]
  • C# 7.0 新特性:基于Tuple的“多”返回值方法
    本文介绍了C# 7.0中基于Tuple的“多”返回值方法的使用。通过对C# 6.0及更早版本的做法进行回顾,提出了问题:如何使一个方法可返回多个返回值。然后详细介绍了C# 7.0中使用Tuple的写法,并给出了示例代码。最后,总结了该新特性的优点。 ... [详细]
  • 本文介绍了为什么要使用多进程处理TCP服务端,多进程的好处包括可靠性高和处理大量数据时速度快。然而,多进程不能共享进程空间,因此有一些变量不能共享。文章还提供了使用多进程实现TCP服务端的代码,并对代码进行了详细注释。 ... [详细]
  • 计算机存储系统的层次结构及其优势
    本文介绍了计算机存储系统的层次结构,包括高速缓存、主存储器和辅助存储器三个层次。通过分层存储数据可以提高程序的执行效率。计算机存储系统的层次结构将各种不同存储容量、存取速度和价格的存储器有机组合成整体,形成可寻址存储空间比主存储器空间大得多的存储整体。由于辅助存储器容量大、价格低,使得整体存储系统的平均价格降低。同时,高速缓存的存取速度可以和CPU的工作速度相匹配,进一步提高程序执行效率。 ... [详细]
  • 本文介绍了作者在开发过程中遇到的问题,即播放框架内容安全策略设置不起作用的错误。作者通过使用编译时依赖注入的方式解决了这个问题,并分享了解决方案。文章详细描述了问题的出现情况、错误输出内容以及解决方案的具体步骤。如果你也遇到了类似的问题,本文可能对你有一定的参考价值。 ... [详细]
  • 开发笔记:计网局域网:NAT 是如何工作的?
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了计网-局域网:NAT是如何工作的?相关的知识,希望对你有一定的参考价值。 ... [详细]
  • 本文介绍了在mac环境下使用nginx配置nodejs代理服务器的步骤,包括安装nginx、创建目录和文件、配置代理的域名和日志记录等。 ... [详细]
  • 本文讨论了clone的fork与pthread_create创建线程的不同之处。进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合。在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性。但是二者之间的通讯需要通过专门的通讯机制,另外通过fork创建子进程系统开销很大。因此,在某些情况下,使用clone或pthread_create创建线程可能更加高效。 ... [详细]
  • Go语言实现堆排序的详细教程
    本文主要介绍了Go语言实现堆排序的详细教程,包括大根堆的定义和完全二叉树的概念。通过图解和算法描述,详细介绍了堆排序的实现过程。堆排序是一种效率很高的排序算法,时间复杂度为O(nlgn)。阅读本文大约需要15分钟。 ... [详细]
  • 李逍遥寻找仙药的迷阵之旅
    本文讲述了少年李逍遥为了救治婶婶的病情,前往仙灵岛寻找仙药的故事。他需要穿越一个由M×N个方格组成的迷阵,有些方格内有怪物,有些方格是安全的。李逍遥需要避开有怪物的方格,并经过最少的方格,找到仙药。在寻找的过程中,他还会遇到神秘人物。本文提供了一个迷阵样例及李逍遥找到仙药的路线。 ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • 配置IPv4静态路由实现企业网内不同网段用户互访
    本文介绍了通过配置IPv4静态路由实现企业网内不同网段用户互访的方法。首先需要配置接口的链路层协议参数和IP地址,使相邻节点网络层可达。然后按照静态路由组网图的操作步骤,配置静态路由。这样任意两台主机之间都能够互通。 ... [详细]
  • JDK源码学习之HashTable(附带面试题)的学习笔记
    本文介绍了JDK源码学习之HashTable(附带面试题)的学习笔记,包括HashTable的定义、数据类型、与HashMap的关系和区别。文章提供了干货,并附带了其他相关主题的学习笔记。 ... [详细]
author-avatar
学银先生_512
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有