原子操作、std::atomic<> 和写入顺序

2023-11-30

GCC 编译如下:

#include <atomic>
std::atomic<int> a; 
int b(0);

void func()
{
  b = 2; 
  a = 1;
}

对此:

func():
    mov DWORD PTR b[rip], 2
    mov DWORD PTR a[rip], 1
    mfence
    ret

所以,为了我澄清一些事情:

  • 任何其他线程将“a”读取为 1 是否保证将“b”读取为 2。
  • 为什么 MFENCE 发生在写入“a”之后而不是之前。
  • 无论如何,对“a”的写入是否保证是一个原子(狭义的、非 C++ 意义上的)操作,这是否适用于所有英特尔处理器?我从这个输出代码中假设是这样。

另外,clang (v3.5.1 -O3) 会执行以下操作:

mov dword ptr [rip + b], 2
mov eax, 1
xchg    dword ptr [rip + a], eax
ret

对于我的小头脑来说,这似乎更简单,但为什么采用不同的方法,每种方法的优点是什么?


我把你的例子放在Godbolt编译器资源管理器,并添加了一些功能读取、递增或组合 (a+=b) 两个原子变量。我也用过a.store(1, memory_order_release);代替a = 1;为了避免获得超出需要的订单,因此它只是 x86 上的一个简单存储。

请参阅下面的(希望是正确的)解释。update: I had “释放”语义与 StoreStore 屏障混淆。我想我修正了所有错误,但可能留下了一些错误。


首先简单的问题:

对“a”的写入保证是原子的吗?

是的,任何线程阅读a将获得旧值或新值,而不是一些写了一半的值。这在 x86 上免费发生以及大多数其他架构,具有适合寄存器的任何对齐类型。 (例如,不int64_t在 32 位上。)因此,在许多系统上,这恰好适用于b这也是大多数编译器生成代码的方式。

有些类型的存储在 x86 上可能不是原子的,包括跨越缓存行边界的未对齐存储。但std::atomic当然保证任何必要的对齐。

读-修改-写操作是有趣的地方。 1000 条评价a+=3一次在多个线程中完成总是会产生a += 3000。如果你得到的可能会更少a不是原子的。

有趣的事实:与普通的有符号类型不同,有符号原子类型保证二进制补码环绕。 C 和 C++ 仍然坚持在其他情况下不定义有符号整数溢出的想法。有些 CPU 没有算术右移,因此保留负数的右移未定义是有道理的,但除此之外,现在所有 CPU 都使用 2 的补码和 8 位字节,这感觉就像是一个荒谬的循环。</rant>


任何其他线程将“a”读取为 1 是否保证将“b”读取为 2。

是的,因为有提供的保证std::atomic.

现在我们进入记忆模型语言及其运行的硬件。

C11 和 C++11 的内存排序模型非常弱,这意味着编译器可以对内存操作重新排序,除非您告诉它不要这样做。 (来源:Jeff Preshing 的弱记忆模型与强记忆模型)。即使 x86 是您的目标机器,您也必须停止编译器对存储重新排序compile时间。 (例如通常你会want编译器提升a = 1跳出循环也写入b.)

默认情况下,使用 C++11 原子类型可为您提供相对于程序其余部分的完全顺序一致性的操作顺序。这意味着它们不仅仅是原子的。请参阅下文,了解将排序放宽到所需的数量,从而避免昂贵的栅栏操作。


为什么 MFENCE 发生在写入“a”之后而不是之前。

商店围栏是 x86 强大内存模型的无操作,因此编译器只需将存储放入b到店之前a实现源代码排序。

完全顺序一致性还要求在以后按程序顺序加载之前,存储是全局排序/全局可见的。

x86 可以在加载后重新排序存储。实际上,发生的情况是,乱序执行会在指令流中看到独立的加载,并在仍在等待数据准备就绪的存储之前执行它。不管怎样,顺序一致性禁止这样做,所以 gcc 使用MFENCE,这是一个完整的屏障,包括 StoreLoad (x86 唯一没有免费的. (LFENCE/SFENCE仅对弱有序操作有用,例如movnt.))

另一种说法是 C++ 文档使用的方式:顺序一致性保证所有线程都能看到same命令。每个原子存储之后的 MFENCE 保证该线程可以看到来自其他线程的存储。否则,我们的负载会在其他线程的负载看到我们的商店之前看到我们的商店。 StoreLoad 屏障 (MFENCE) 会延迟我们的加载,直到需要首先发生的存储之后。

ARM32 汇编语言b=2; a=1; is:

# get pointers and constants into registers
str r1, [r3]     # store b=2
dmb sy           # Data Memory Barrier: full memory barrier to order the stores.
   #  I think just a StoreStore barrier here (dmb st) would be sufficient, but gcc doesn't do that.  Maybe later versions have that optimization, or maybe I'm wrong.
str r2, [r3, #4] # store a=1  (a is 4 bytes after b)
dmb sy           # full memory barrier to order this store wrt. all following loads and stores.

我不知道 ARM asm,但到目前为止我发现通常它是op dest, src1 [,src2],但加载和存储始终首先是寄存器操作数,然后是内存操作数。如果您习惯使用 x86,这真的很奇怪,其中内存操作数可以是大多数非向量指令的源或目标。加载立即常量也需要大量指令,因为固定指令长度只为 16b 的有效负载留下空间movw(移动单词)/movt(移至顶部)。


释放/获取

The release and acquire单向内存屏障的命名来自于锁:

  • 一个线程修改一个共享数据结构,然后releases一把锁。在所有加载/存储其保护的数据之后,解锁必须是全局可见的。 (存储存储 + 加载存储)
  • 另一个线程acquires锁(读取,或带有释放存储的 RMW),并且必须在获取变得全局可见后对共享数据结构执行所有加载/存储。 (加载加载+加载存储)

请注意,std:atomic 即使对于与加载获取或存储释放操作略有不同的独立栅栏也使用这些名称。 (参见下面的atomic_thread_fence)。

发布/获取语义比生产者-消费者所需的更强大。这仅需要单向 StoreStore(生产者)和单向 LoadLoad(消费者),无需 LoadStore 排序。

例如,受读取器/写入器锁保护的共享哈希表需要获取-加载/释放-存储原子读取-修改-写入操作来获取锁。 x86lock xadd是一个完整的屏障(包括 StoreLoad),但 ARM64 有加载链接/存储条件的加载获取/存储释放版本,用于执行原子读取-修改-写入。据我了解,这避免了对 StoreLoad 屏障的需要,甚至是锁定。


使用较弱但仍然足够的排序

写入到std::atomic默认情况下,类型相对于源代码中的所有其他内存访问(加载和存储)进行排序。您可以控制强加的排序std::memory_order.

在您的情况下,您只需要生产者确保商店以正确的顺序在全球范围内可见,即商店之前的 StoreStore 屏障a. store(memory_order_release)包括这个以及更多。std::atomic_thread_fence(memory_order_release)只是所有商店的单向 StoreStore 屏障。 x86 免费提供 StoreStore,因此编译器所要做的就是按源代码顺序放置存储。

尤其是,使用 Release 而不是 seq_cst 将获得巨大的性能优势。在像 x86 这样的架构上,发布是便宜/免费的。如果无争议的情况很常见,则更是如此。

读取原子变量还可以强制加载相对于所有其他加载和存储的完全顺序一致性。在 x86 上,这是免费的。 LoadLoad 和 LoadStore 屏障是无操作的,并且隐含在每个内存操作中。您可以使用以下方法使代码在弱有序 ISA 上更加高效a.load(std::memory_order_acquire).

请注意,std::atomic 独立栅栏函数混淆地重用了 StoreStore 和 LoadLoad 栅栏的“获取”和“释放”名称,这些栅栏至少以所需的方向对所有存储(或所有加载)进行排序。在实践中,它们通常会发出作为 2 路 StoreStore 或 LoadLoad 屏障的硬件指令。This doc是成为当前标准的提案。您可以看到 memory_order_release 如何映射到#LoadStore | #StoreStore在 SPARC RMO 上,我认为其中部分包含在内,因为它分别具有所有屏障类型。 (嗯,cppref 网页只提到了排序存储,而不是 LoadStore 组件。不过,它不是 C++ 标准,所以也许完整的标准说了更多。)


memory_order_consume对于这个用例来说不够强大。这个帖子谈论您使用标志来指示其他数据已准备就绪的情况,并谈论memory_order_consume.

consume如果你的标志是一个指向的指针就足够了b,甚至是指向结构体或数组的指针。但是,没有编译器知道如何进行依赖项跟踪以确保它将事物按正确的顺序放入 asm 中,因此当前的实现始终将consume as acquire。这太糟糕了,因为除了 DEC alpha(和 C++11 的软件模型)之外的每个体系结构都免费提供此排序。根据 Linus Torvalds 的说法,只有少数 Alpha 硬件实现实际上可以进行这种重新排序,因此各处所需的昂贵屏障指令对于大多数 Alpha 来说纯粹是缺点。

生产者仍然需要使用release语义(StoreStore 屏障),以确保更新指针时新的有效负载可见。

使用以下代码编写代码并不是一个坏主意consume,如果您确定您理解其中的含义并且不依赖于任何consume并不能保证。将来,一旦编译器变得更加智能,即使在 ARM/PPC 上,您的代码也将在没有障碍指令的情况下进行编译。实际的数据移动仍然必须在不同 CPU 上的缓存之间进行,但在弱内存模型机器上,您可以避免等待任何不相关的写入可见(例如生产者中的暂存缓冲区)。

请记住你实际上无法测试memory_order_consume实验性地编码,因为当前的编译器为您提供了比代码请求更强的排序。

无论如何,通过实验来测试这些都非常困难,因为它对时间敏感。此外,除非编译器重新排序操作(因为您未能告诉它不要这样做),否则生产者-消费者线程在 x86 上永远不会出现问题。您需要在 ARM 或 PowerPC 或其他设备上进行测试,甚至尝试查找实际中发生的排序问题。


参考:

  • https://gcc.gnu.org/bugzilla/show_bug.cgi?id=67458:我报告了我发现的 gcc bugb=2; a.store(1, MO_release); b=3;生产a=1;b=3在 x86 上,而不是b=3; a=1;

  • https://gcc.gnu.org/bugzilla/show_bug.cgi?id=67461:我还报告了 ARM gcc 使用两个的事实dmb sy连续为a=1; a=1;,x86 gcc 可能可以减少 mfence 操作。我不确定是否mfence每个存储之间需要保护信号处理程序不做出错误的假设,或者只是缺少优化。

  • C++11 中 memory_order_consume 的用途(上面已链接)恰好涵盖了使用标志在线程之间传递非原子有效负载的情况。

  • StoreLoad 屏障 (x86 mfence) 的用途:一个演示需求的工作示例程序:http://preshing.com/20120515/memory-reordering-caught-in-the-act/

  • 数据依赖屏障(只有 Alpha 需要这种类型的显式屏障,但 C++ 可能需要它们来防止编译器执行推测性加载):http://www.mjmwired.net/kernel/Documentation/memory-barriers.txt#360
  • 控制依赖障碍:http://www.mjmwired.net/kernel/Documentation/memory-barriers.txt#592

  • Doug Lea说x86只需要LFENCE对于用“流”写入的数据,如下所示movntdqa or movnti。 (NT = 非时间)。除了绕过缓存之外,x86 NT 加载/存储还具有弱有序语义。

  • http://preshing.com/20120913/acquire-and-release-semantics/

  • http://preshing.com/20120612/an-introduction-to-lock-free-programming/(指向他推荐的书籍和其他东西)。

  • 有趣的Realworldtech 上的主题关于无处不在的障碍或强大的内存模型是否更好,包括数据依赖性在硬件中几乎是免费的这一点,因此跳过它并给软件带来很大的负担是愚蠢的。 (Alpha(和 C++)没有的东西,但其他一切都有)。在 Linus Torvalds 开始解释他的论点的更详细/技术原因之前,先回顾一下其中的几篇文章,看看 Linus Torvalds 的有趣的侮辱。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

原子操作、std::atomic<> 和写入顺序 的相关文章

  • 警告:从指针目标类型中丢弃“const”限定符

    没有const char s意味着 s 是一个指向常量 char 的指针 那么为什么它给我这个警告 我并不是想改变价值观 在第一个函数中警告是return discards const qualifiers from pointer tar
  • C++ 有像 Pascal 一样的“with”关键字吗?

    withPascal 中的关键字可用于快速访问记录的字段 有人知道 C 是否有类似的东西吗 前任 我有一个包含许多字段的指针 但我不想这样输入 if pointer gt field1 pointer gt field2 pointer g
  • 如何将 C++ 类包装在基于 C 的 dll 或基于 CLI 的 dll 中?

    我被告知将我用 C 编写的类导入到 dll 中 然后在 c 应用程序中使用该 dll 下列的本指南 https stackoverflow com questions 4555961 how to use a class in dll我创建
  • 禁用除滚动之外的 DataGridView

    我如何配置 datagridview 以便用户只能在行中移动并使用滚动 而没有其他 如果我禁用网格不允许我使用滚动 将您的 datagridview 设置为只读 这将禁用任何编辑 dataGridView1 ReadOnly true 在你
  • TestMethod:异步任务 TestSth() 不适用于 .NET 4.0

    我正在尝试使用 NET 4 0 BCL Async 和 MsTest 运行异步测试方法 看来这个设置不能处理 测试方法 异步Task测试Sth 由于测试用例资源管理器中缺少条目 将签名更改为异步后void 我可以运行测试用例 但结果错误 根
  • 将占位符文本添加到文本框

    我正在寻找一种将占位符文本添加到文本框的方法 就像在 html5 中使用文本框一样 IE 如果文本框没有文本 则会添加文本Enter some text here 当用户单击它时 占位符文本消失并允许用户输入自己的文本 如果文本框失去焦点并
  • 实体框架7审计日志

    我正在将一个旧项目移植到 ASP NET 5 和 Entity Framework 7 我使用数据库优先方法 DNX 脚手架 来创建模型 旧项目基于Entity Framework 4 审计跟踪是通过重写实现的SaveChanges的方法D
  • initializer_list 和默认构造函数重载决策

    include
  • 如何使用 C# 代码使用超链接的 onClick 事件?

    我正在尝试为页面中的超链接添加条件 而不是仅仅使用特定的链接 例如 a href help Tutorial html Tutorial a 我想为不同的用户显示不同的页面 例如 如果用户以管理员身份登录 他们将看到与普通用户不同的链接 我
  • 使用 cudamalloc()。为什么是双指针?

    我目前正在浏览有关的教程示例http code google com p stanford cs193g sp2010 http code google com p stanford cs193g sp2010 学习CUDA 演示的代码 g
  • 为什么 GCC 6.3 在没有显式 C++11 支持的情况下编译此 Braced-Init-List 代码?

    我有一个问题大括号括起来的列表的不同含义 https stackoverflow com q 37682392 2642059 我知道C 03不支持C 11initializer list 然而 即使没有 std c 11编译器标志 gcc
  • fscanf 和 EOF 中的否定扫描集

    我的文件中有一个以逗号分隔的字符串列表 姓名 1 姓名 2 姓名 3 我想跳过所有逗号来阅读这些名字 我写了以下循环 while true if fscanf file my string 1 break 然而 它总是比预期多执行一次 给定
  • C# - 命名空间内的类型声明

    在命名空间内而不是在类中声明类型的可能用途是什么 For ex namespace Test public delegate void Ispossible 这是有效的并且不会产生任何编译错误 但我无法想象为什么我们会以这种方式声明它而不是
  • 统一;随机物体移动[关闭]

    Closed 这个问题需要多问focused help closed questions 目前不接受答案 我正在制作一款机器人战斗游戏 我希望敌人随机移动 然后有时会向敌人移动 我希望运动包含在其中的代码 else if avoid fal
  • Intel 和 AMD 处理器有相同的汇编程序吗?

    C语言被用来编写Unix以实现可移植性 使用不同编译器编译的同一个C语言程序会产生不同的机器指令 为什么 Windows 操作系统能够在两者上运行Intel https en wikipedia org wiki Intel and AMD
  • 无效的模板相关成员函数模板推导 - 认为我正在尝试使用 std::set

    我有一个继承自基类模板的类模板 基类模板有一个数据成员和一个成员函数模板 我想从我的超类中调用它 我知道为了消除对成员函数模板的调用的歧义 我必须使用template关键字 我必须明确引用this在超级班里 this gt base mem
  • 编译器什么时候内联函数?

    在 C 中 函数仅在显式声明时才内联inline 或在头文件中定义 或者编译器是否允许内联函数 因为他们认为合适 The inline关键字实际上只是告诉链接器 或告诉编译器告诉链接器 同一函数的多个相同定义不是错误 如果您想在标头中定义函
  • 网页执行回发时如何停止在注册表单上?

    我正在做我的最后一年的项目 其中 我在一页上有登录和注册表单 WebForm 当用户点击锚点时Sign Up下拉菜单ddlType 隐藏 和文本框 txtCustName txtEmail and txtConfirmPassword 显示
  • 请解释为什么Java和C对此代码给出不同的答案

    public class Test public static void main String args int i 10 i i System out println value of i is i 输出是 10 当我在中执行类似的代码
  • 如何获取通过网络驱动器访问的文件的 UNC 路径?

    我正在 VC 中开发一个应用程序 其中网络驱动器用于访问文件 驱动器由用户手动分配 然后在应用程序中选择驱动器 这会导致驱动器并不总是映射到相同的服务器 我该如何获取此类文件的 UNC 路径 这主要是为了识别目的 这是我用来将普通路径转换为

随机推荐