分享

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

 印度阿三17 2019-09-16

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

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

解决方法:

C doesn’t support something like lock-free std::atomic<double>

实际上,C 11 std :: atomic< double>在典型的C实现上是无锁的,并且在x86上使用float / double进行无锁编程时几乎可以暴露你在asm中可以做的所有事情(例如,load,store和CAS足以实现任何操作:Why isn’t atomic double fully implemented).当前的编译器并不总是编译原子< double>但是效率很高.

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

一些非x86硬件支持float / double的原子添加,而C p0020是一个向C的std :: atomic< float>添加fetch_add和operator = / – =模板特化的建议. /< double>.

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

x86 natually-aligned loads and stores are atomic up to 8 bytes, even x87 or SSE.(例如movsd xmm0,[some_variable]是原子的,即使在32位模式下也是如此).事实上,gcc使用x87 fild / fistp或SSE 8B加载/存储来实现std :: atomic< int64_t>以32位代码加载和存储.

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

#include <atomic>
std::atomic<double> 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./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(更多细节见Can num be atomic for ‘int num’?).对于其他事情,例如atomic< struct>或原子< double>,x86上的唯一选项是使用cmpxchg(或TSX)的重试循环.

Atomic compare-and-swap (CAS)可用作任何原子RMW操作的无锁构建块,最大硬件支持的CAS宽度.在x86-64上,这是16字节的cmpxchg16b(在某些第一代AMD K8上不可用,因此对于gcc,你必须使用-mcx16或-march =无论启用它).

gcc为exchange()做了最好的asm:

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

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

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

在C中可能没有办法表达这一点,这会让编译器在使用IEEE FP的目标上为你做这件事.因此,如果你想要它,你可能必须自己使用type-punning to atomic< uint64_t>等等,并检查FP字节顺序是否与整数字节序等相匹配等(或者仅对x86进行.大多数其他目标都有LL / SC而不是内存目的地锁定操作.)

can’t yet support something like atomic AVX/SSE vector because it’s CPU-dependent

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

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

Per-element atomicity of vector load/store and gather/scatter?

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

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

16B原子存储和RMW都可以使用锁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术语中:

原子&LT __ m128d&GT即使对于只读或只写操作(使用cmpxchg16b),即使以最佳方式实现,也会很慢.原子&LT __ m256d&GT甚至无法锁定.

alignas(64)atomic< double> shared_buffer将[1024];从理论上讲,它仍然允许对读取或写入它的代码进行自动矢量化,只需要在double上使用movq rax,xmm0和xchg或cmpxchg作为原子RMW. (在32位模式下,cmpxchg8b可以工作.)尽管如此,你几乎肯定不会从编译器中获得好的asm!

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

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

来源:https://www./content-4-454701.html

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多