【问题标题】:Are memory barriers necessary for atomic reference counting shared immutable data?原子引用计数共享不可变数据是否需要内存屏障?
【发布时间】:2011-02-05 15:42:02
【问题描述】:

我有一些不可变的数据结构,我想使用引用计数来管理它们,在 SMP 系统上的线程之间共享它们。

发布代码如下:

void avocado_release(struct avocado *p)
{
    if (atomic_dec(p->refcount) == 0) {
        free(p->pit);
        free(p->juicy_innards);
        free(p);
    }
}

atomic_dec 需要内存屏障吗?如果有,是什么样的内存屏障?

附加说明:该应用程序必须在 PowerPC 和 x86 上运行,因此欢迎提供任何特定于处理器的信息。我已经知道 GCC atomic builtins。至于不变性,引用计数是唯一在对象的持续时间内改变的字段。

【问题讨论】:

  • 澄清一下,我的兴趣更多是出于好奇,而不是出于对可行解决方案的需求。

标签: c multithreading atomic memory-barriers refcounting


【解决方案1】:

您是打算实现自己的atomic_dec,还是只是想知道系统提供的函数是否会按照您的意愿运行?

作为一般规则,系统提供的原子增量/减量工具将应用所需的任何内存屏障来做正确的事情。您通常不必担心内存障碍,除非您正在做一些古怪的事情,例如实现自己的无锁数据结构或 STM 库。

【讨论】:

  • 我想知道在这种情况下是否需要内存屏障,以及为什么。
  • +1 "something" 将需要同步对 refcount 字段的访问。无论那个“东西”实际上是一个内存屏障,还是另一个类似的缓存操作,都需要遍历 CPU 规范和/或检查发出的代码。它不必是完整的缓存刷新,也许 CPU 只会使使用的单个缓存行无效。编译器和 CPU 都必须确保指令不会在递减过程中重新排序,但基于递减结果的条件几乎可以确保无论如何。
  • @Dietrich:在这种情况下,不,因为后续操作取决于递减的结果,因此编译器不可能以有问题的方式重新排序。此外,引用计数的本质是,当计数达到零时,可能只有一个线程可以访问相关对象(即没有错误)。
  • @Steve:我只提到它是因为人们在讨论多线程正确性时似乎过度担心缓存。像 x86 系统这样的现代多处理器将在硬件中处理这一切。在缓存一致的系统中,如果您正在破解内核或进行 DMA 传输的设备的驱动程序,您只需要担心缓存刷新。当然,这对性能很重要,但对正确性并不重要。
  • 当然:您是否知道多核 PowerPC 是否一定有相干缓存?但是你是对的,原子就是原子的,无论它是通过显式缓存失效还是一致缓存实现的,或者其他什么,都很少影响应用程序代码。假设一致的缓存,你可以做一些事情:你是否应该是有问题的。
【解决方案2】:

在 x86 上,它将变成以 lock 为前缀的汇编指令,例如 LOCK XADD
作为一条指令,它是不可中断的。作为一个附加的“功能”,lock 前缀会导致完全的内存屏障:

“...锁定操作序列化所有未完成的加载和存储操作(即等待它们完成)。” ...“锁定操作相对于所有其他内存操作和所有外部可见事件是原子的。只有取指和页表访问才能传递锁定指令。锁定指令可用于同步一个处理器写入和另一个处理器读取的数据。” - Intel® 64 and IA-32 Architectures Software Developer’s Manual,第 8.1.2 章。

内存屏障实际上在 x86/x64 上的 the .NETthe JAVA JIT 中实现为虚拟 LOCK ORLOCK AND,因为 mfence 在许多 CPU 上速度较慢,即使它保证可用,就像在 64 位模式下一样。 (Does lock xchg have the same behavior as mfence?)
因此,无论您喜欢与否,您都可以在 x86 上拥有一个完整的围栏作为额外的奖励。 :-)

在 PPC 上,情况有所不同。 LL/SC 对 - lwarx & stwcx - 内部带有减法,可用于将内存操作数加载到寄存器中,减一,然后如果目标位置没有其他存储,则将其写回,或者重试整个循环如果有的话。 LL/SC 可以被中断(意味着它将失败并重试)。
这也不意味着自动全围栏。
但是,这不会以任何方式损害计数器的原子性。
这只是意味着在 x86 的情况下,您碰巧也获得了一个栅栏,“免费”。
在 PPC 上,可以通过发出 (lw)sync @987654328 来插入(部分或)完整栅栏@.

总而言之,原子计数器正常工作不需要显式内存屏障。

【讨论】:

  • @Rachid K. - 感谢您修复错别字,但实际代码通常应使用代码格式,例如 x86 lock 前缀。 (它是代码而不仅仅是一个名称,因为lock 是使用它的 asm 语法的一部分。)斜体在这里不合适。 (虽然斜体在段落中间的视觉干扰较小,所以我在您对布鲁斯的回答的编辑中保留了这种方式。在我自己的回答中,我倾向于在中间使用全大写的寄存器名称或指令助记符当我不想要代码格式化许多单词的视觉噪音时的段落。)
【解决方案3】:

区分原子访问(保证值的读取/修改/写入作为一个原子单元执行)与内存重新排序非常重要。

内存屏障防止读取和写入的重新排序。重新排序与原子性完全正交。例如,在 PowerPC 上,如果您实现最有效的原子增量,那么它不会阻止重新排序。如果你想防止重新排序,那么你需要一个 lwsyncsync 指令,或者一些等效的高级(C++ 11?)内存屏障。

声称“编译器不可能以有问题的方式重新排序”作为一般性陈述似乎很幼稚,因为编译器优化可能非常令人惊讶,并且因为 CPU(尤其是 PowerPC/ARM/Alpha/MIPS)积极地重新排序内存操作.

连贯的缓存也不能拯救你。请参阅 https://preshing.com/archives/ 了解内存重新排序的实际工作原理。

然而,在这种情况下,我相信答案是不需要障碍。这是因为对于这种特定情况(引用计数),不需要在引用计数和对象中的其他值之间建立关系。一个例外是引用计数为零时。此时,确保来自其他线程的所有更新对当前线程可见非常重要,因此可能需要读取-获取屏障

【讨论】:

猜你喜欢
  • 2015-12-17
  • 2012-09-08
  • 2011-10-12
  • 1970-01-01
  • 2011-06-09
  • 2014-01-20
  • 2015-03-01
  • 2017-01-05
  • 1970-01-01
相关资源
最近更新 更多