在使用原子引用计数共享不可变数据时,是否需要内存屏障?

16

我有一些不可变数据结构,希望使用引用计数来管理它们,在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原子内建函数。 至于不可变性,引用计数是对象在其生命周期中唯一更改的字段。


作为澄清,我对此更多的是出于好奇而非需要一个可行的解决方案。 - Dietrich Epp
3个回答

19
在x86上,它将变成一个带有lock前缀的汇编指令,如LOCK XADD。
由于是单个指令,因此不可中断。作为一个额外的“特性”,lock前缀会导致完整的内存屏障:

"...锁定操作序列化所有未完成的加载和存储操作。" ..."锁定操作原子地与所有其他内存操作和所有外部可见事件相关。只有指令获取和页面表访问可以通过锁定指令。锁定指令可用于同步由一个处理器写入并由另一个处理器读取的数据。" - Intel® 64和IA-32架构软件开发人员手册,第8.1.2章。

内存屏障实际上在.NETJAVA JIT中被实现为虚拟的LOCK ORLOCK AND,因为即使在保证可用的情况下(例如在64位模式下),使用mfence会使许多CPU变慢。(lock xchg是否具有与mfence相同的行为?)
所以,在x86上你有一个完整的栅栏作为额外的奖励,无论你喜欢与否。 :-)

在 PPC 上,情况有所不同。可以使用带有减法的 LL/SC 对 - lwarx & stwcx - 将内存操作数加载到寄存器中,减去一个,然后如果没有其他存储到目标位置,则将其写回,否则重试整个循环。LL/SC 可以被中断(意味着它会失败并重试)。
这也不意味着自动进行完整的屏障。
但这并不会以任何方式影响计数器的原子性。
这只是意味着在 x86 的情况下,您还可以免费获得一个屏障。
在 PPC 上,可以通过发出 (lw)sync 指令 来插入 (部分或) 完整的屏障。 总之,显式内存屏障对于原子计数器正常工作并不必要。

@Rachid K. - 感谢您纠正错别字,但实际代码通常应使用代码格式,例如x86的 lock 前缀。 (它是代码而不仅仅是名称,因为 lock 是使用它的汇编语法的一部分。)这里并不太适合使用斜体字。 (尽管在段落中间斜体字会更少视觉干扰,因此我将其保留在您对Bruce's答案的编辑中。 在我的回答中,当我不想使许多单词的代码格式化造成视觉噪音时,我倾向于在段落中间使用全大写注册名或指令助记符。) - Peter Cordes

10

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

内存屏障可以防止读取和写入的重排序。重排序与原子性完全无关。例如,在PowerPC上,如果你实现了最有效的原子递增,则不会防止重排序。如果你想要防止重排序,则需要使用lwsyncsync指令,或者一些高级等效的(C++ 11?)内存屏障。

声称“编译器没有可能以有问题的方式重新排序”似乎是一种幼稚的通用陈述,因为编译器优化可能相当令人惊讶,并且CPU(特别是PowerPC / ARM / Alpha / MIPS)会积极地重新排序内存操作。

一致的缓存也无法解决问题。请参见https://preshing.com/archives/了解内存重排序的真正工作原理。

在这种情况下,我认为不需要任何屏障。这是因为对于这个特定的情况(引用计数),参考计数与对象中的其他值之间没有必要建立关系。唯一的例外是当引用计数达到零时。此时,重要的是确保来自其他线程的所有更新对当前线程可见,因此可能需要使用读取-获取屏障。

此外,还请参考我几年前撰写的这篇论文: https://msdn.microsoft.com/zh-cn/library/windows/desktop/ee418650(v=vs.85).aspx - Bruce Dawson

3
你是否打算实现自己的atomic_dec函数,还是只是想知道系统提供的函数是否符合你的要求?一般来说,系统提供的原子增加/减少功能会应用所需的所有内存栅障,以确保正确性。通常情况下,你不必担心内存栅障,除非你正在实现自己的无锁数据结构或 STM 库等奇怪的东西。请注意,保留了 HTML 标签。

1
我想知道在这种情况下是否需要内存屏障,以及为什么。 - Dietrich Epp
+1 "something"将需要同步访问refcount字段。无论这个“something”是字面上的内存屏障,还是其他类似的缓存操作,都需要查阅CPU规格说明书和/或检查发出的代码。它不必是完全的缓存刷新,也许CPU只会使单个缓存行失效。编译器和CPU都必须确保指令在递减过程中不被重新排序,但基于递减结果的条件语句几乎可以确保这一点。 - Steve Jessop
2
@Dietrich:在这种情况下,不会,因为后续操作是有条件的,取决于递减的结果,因此编译器没有可能以有问题的方式重新排序事物。此外,refcount 的性质是,当计数达到零时,只有一个线程可能访问相关对象(除非存在错误)。 - Marcelo Cantos
1
@Steve:我之所以提到这个,是因为人们似乎在讨论多线程正确性时过分担心缓存。像x86系统这样的现代多处理器将在硬件上全面解决它。在具有高速缓存一致性的系统中,只有在为执行 DMA 传输的内核或驱动程序进行编码时才需要担心缓存刷新。当然,这对性能很重要,但不影响正确性。 - Per Knytt
1
你知道多核PowerPC是否必须具有一致的缓存吗?但是你说得对,原子操作就是原子操作,无论它是通过显式缓存失效还是一致的缓存实现,很少影响应用程序代码。假设具有一致的缓存,你可以做一些事情:但是否应该这样做还有疑问。 - Steve Jessop
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接