如何“获取”和“消费”的记忆命令不同,什么时候“消费”更可取?
C ++ 11标准定义了一个包含内存顺序的内存模型(1.7,1.10),这些顺序大体上是“顺序一致”,“获取”,“消耗”,“释放”和“放松”。 同样粗略地说,一个程序只有在没有竞争的情况下才是正确的,如果所有的行为都可以按照一个行为发生的顺序进行,那么就发生在另一个行为之前 。 一个动作X发生的方式 – 在一个动作Y 之前 ,要么是X在Y之前(在一个线程之内)被sorting,要么在X之前发生X之间的线程 。 后者的条件是什么时候给出的
- X与Y同步,或
- X在Y之前是依赖项sorting的。
当X是一个primefaces存储,在某些primefacesvariables上有“释放”顺序,而Y是一个在同一个variables上具有“获取”顺序的primefaces载入时,会发生同步 。 依赖性sorting – 之前发生的是类似的情况,其中Y是加载“消费”sorting(和适当的内存访问)。 同步概念扩展了前后关系,在一个线程内彼此在一个线程之间传递,而在另一个线程之前,只是通过一个被严格子集的sequenced-before被调用的进位依赖 ,遵循一套较大的规则,特别是可以用std::kill_dependency
中断。
那么,“依赖sorting”这个概念的目的是什么呢? 它提供了什么优势比简单的顺序 / 同步 – 与sorting? 由于规则更严格,我认为可以更有效地实施。
你能举一个例子,从释放/获取到释放/消耗的切换是正确的,并提供了一个不平凡的优势? 何时将std::kill_dependency
提供改进? 高级别的参数会很好,但是硬件特定差异的奖励点。
N2492引入了数据依赖性sorting, 原因如下:
目前的工作草案(N2461)在某些现有硬件上不支持可扩展性,这有两个重要的用例。
- 读访问很less写入的并发数据结构
在操作系统内核和服务器风格的应用程序中,罕见的并发数据结构是相当普遍的。 示例包括表示外部状态(如路由表),软件configuration(当前加载的模块),硬件configuration(当前正在使用的存储设备)以及安全策略(访问控制权限,防火墙规则)的数据结构。 读写比例超过十亿分之一是相当普遍的。
- 发布 – 订阅语义的指针介导的发布
线程之间的很多通信是由指针调用的,生产者发布一个消费者可以通过其访问信息的指针。 没有完整的获取语义,访问这些数据是可能的。
在这种情况下, 使用线程间数据依赖性sorting已经导致了数量级的加速,并在支持线程间数据依赖性sorting的机器上的可伸缩性方面得到了类似的改进。 这样的加速是可能的,因为这样的机器可以避免昂贵的锁获取,primefaces指令或存储器防护,否则需要。
强调我的
那里提供的激励用例是来自Linux内核的rcu_dereference()
负载消耗与负载获取非常相似,只不过它仅在依赖于负载消耗的数据依赖的expression式评估中引发了发生之前的关系。 用kill_dependency
包装一个expression式kill_dependency
产生一个不再依赖load-consume的值。
关键的用例是编写者按顺序构造一个数据结构,然后将一个共享指针摆动到新结构(使用release
或acq_rel
primefaces)。 阅读器使用load-consume来读取指针,并将其解引用到数据结构中。 取消引用会创build一个数据依赖关系,所以读者可以保证看到初始化的数据。
std::atomic<int *> foo {nullptr}; std::atomic<int> bar; void thread1() { bar = 7; int * x = new int {51}; foo.store(x, std::memory_order_release); } void thread2() { int *y = foo.load(std::memory_order_consume) if (y) { assert(*y == 51); //succeeds // assert(bar == 7); //undefined behavior - could race with the store to bar // assert(kill_dependency(*y) + bar == 58) // undefined behavior (same reason) assert(*y + bar == 58); // succeeds - evaluation of bar pulled into the dependency } }
提供负载消耗有两个原因。 主要原因是ARM和Power负载保证消耗,但需要额外的屏蔽才能将其转化为获取。 (在x86上,所有的加载都是获取的,所以消耗在初始编译时没有提供直接的性能优势)。次要的原因是编译器可以在不消耗数据的情况下移动以后的操作,直到消耗之前,对于获取。 (启用这样的优化是构build所有这些内存sorting语言的重要原因。)
用kill_dependency
包装一个值允许计算一个expression式,这个expression式取决于要在load-consume之前移动的值。 例如,当这个值是一个先前读取的数组的索引时,这是很有用的。
请注意,使用消费会导致不再传递的发生之前的关系(尽pipe它仍然是非循环的)。 例如,存储到bar
发生在商店之前foo,这发生在y
的取消引用之前,这发生在读取bar
(在注释掉的assert中)之前,但是存储到bar
不会在读取之前发生的bar
。 这导致了一个更复杂的发生的定义,但你可以想象它是如何工作的(从Sequenced-before开始,然后通过任意数量的release-consume-dataDependency或release-acquire-sequencedBefore链接传播)
杰夫Preshing有一个伟大的博客回答这个问题。 我不能自己添加任何东西,但想想有人想知道消费与获得应该读他的post:
http://preshing.com/20140709/the-purpose-of-memory_order_consume-in-cpp11/
他在三个不同的体系结构中显示了具有相应基准汇编代码的特定C ++示例。 与memory_order_acquire
相比, memory_order_consume
可能在PowerPC上提供3倍的加速,在ARM上提供1.6倍的加速,而x86上的加速可以忽略不计。 问题的关键在于,在他写这篇文章的时候,只有GCC实际上对待获取语义和获取语义有任何不同,可能是因为一个bug。 尽pipe如此,它表明如果编译器编写者能够弄清楚如何利用它,那么加速是可用的。
我想logging一个部分的发现,尽pipe这不是一个真正的答案,并不意味着没有一个正确的答案的大恩惠。
在盯着1.10一段时间,特别是第11段非常有帮助的说明之后,我认为这并不是那么难。 与 (以后称为s / w)和依赖顺序之前 (dob) 同步的最大区别在于可以通过任意连接顺序之前 (s / b)和s / w来build立之前发生的关系,但是对于dob来说不是这样 。 注意线程之间的一个定义发生在 :
A
同步X
和X
同步
但是的类似声明A
是在丢失X
之前依赖项sorting的 !
因此,释放/获取(即S / W),我们可以命令任意事件:
A1 s/b B1 Thread 1 s/w C1 s/b D1 Thread 2
但是现在考虑一下这样一个任意的事件序列:
A2 s/b B2 Thread 1 dob C2 s/b D2 Thread 2
在这个后遗症中, A2
发生在 C2
之前是正确的(因为A2
是s / b B2
和B2
之间的线程是在 C2
之前发生的,但是我们可以争辩说,你永远不能真正说出!)。 然而, A2
发生在 D2
之前 是不正确的 。 除非事实上认为C2
对 D2
具有依赖性 , 否则事件A2
和D2
不是相互sorting的。 这是一个更严格的要求,如果没有这个要求, A2
到D2
不能被“跨越”释放/消费对。
换句话说,一个释放/消耗对只传播一个动作的顺序,从一个到另一个传递一个依赖。 所有不依赖的东西都不是在释放/消耗对中sorting的。
此外,请注意,如果我们追加一个最终的更强大的发布/获取对,sorting会被恢复:
A2 s/b B2 Th 1 dob C2 s/b D2 Th 2 s/w E2 s/b F2 Th 3
现在,按照引用的规则, D2
之间的线程发生在 F2
之前 ,因此C2
和B2
也是这样,所以A2
发生在 F2
之前 。 但请注意, A2
和D2
之间仍然没有sorting – sorting仅在A2
和之后的事件之间。
总而言之,依赖携带是通用sorting的严格子集,而释放/消费对仅在具有依赖性的行为之间提供sorting。 只要不需要更强的sorting(例如通过通过发布/获取对),理论上可能有额外的优化,因为不在依赖链中的所有内容都可以自由地重新sorting。
也许这是一个有意义的例子吗?
std::atomic<int> foo(0); int x = 0; void thread1() { x = 51; foo.store(10, std::memory_order_release); } void thread2() { if (foo.load(std::memory_order_acquire) == 10) { assert(x == 51); } }
正如所写的,代码是无竞争的,并且断言将保持,因为释放/获取对或者在加载之前决定存储x = 51
。 然而,通过将“获取”改变为“消耗”,这将不再是真实的,程序将在x
上进行数据竞赛,因为x = 51
不存在对商店的依赖性。 优化点是这个商店可以自由地重新sorting而不用担心foo
在做什么,因为没有依赖关系。