为什么在缓存行大小通常为64字节的情况下,sizeof std::mutex等于40?

6

在gcc和clang的主干版本中,以下静态断言通过

#include<mutex>
int main(){
    static_assert(sizeof(std::mutex)==40);
}

由于x86 CPU具有64字节的缓存行,我预计互斥锁的大小应该为64,以避免虚假共享。那么为什么其大小仅为“40”字节呢?
注意:我知道大小也会影响性能,但是程序中很少有大量的互斥锁,因此与虚假共享的代价相比,大小开销似乎可以忽略不计。
注意:有一个类似的问题询问为什么std::mutex如此之大,而我要问的是为什么它这么小 :)
注:MSVC 16.7的大小为80。

如果我们假设std:mutex只是一个被吹嘘的结构体,而结构体的默认最小对齐方式取决于其最大字段而不是整个结构体的大小;那么可以合理地假设sizeof(std:mutex)几乎与最小对齐无关,甚至更少地表明了最佳对齐方式。相反,如果您想要64字节对齐,则无论结构体大小如何(例如使用类似于alignas(64)的东西),都需要64字节对齐;并且sizeof()大多无关紧要,应该使用std::alignment_of()代替。 - Brendan
这里的“false shared”是什么?虚假共享的概念如何适用于被共享的互斥量? - Dan M.
2
@DanM.:同一缓存行中的其他数据,包括可能由互斥锁保护的共享数据。如果其他线程试图获取锁并不断地 hammering on the mutex,那么包含它的缓存行将倾向于翻转到共享状态,甚至从所有者的 L1d 缓存中无效。问题是建议 alignof(mutex) = sizeof(mutex) = std::hardware_destructive_interference_size 或类似的设置,以确保互斥锁有自己的缓存行。(请注意,如果你要为其修复大小,则 hw_destructive_... 在某些现代 x86-64 上应该为 128,因为存在相邻行 HW prefetch) - Peter Cordes
@DanM。彼得·科德斯说的就是我所问的(尽管他解释得比我好)。 - NoSenseEtAl
2个回答

5
在不需要填充的情况下强制填充将会导致不良设计。如果用户没有有用的内容放在其余的缓存行中,他们总是可以进行填充。如果通常情况下轻度竞争,您可能希望它与所保护的数据位于同一缓存行中;只有一个缓存行来回弹跳,而不是在获取锁后访问共享数据时出现第二个缓存未命中。这在细粒度锁定中很常见,其中许多对象都有自己的std::mutex,并使其保持较小更有益。在读者尝试获取锁与锁所有者获得所有权后写入共享数据之间存在严重竞争时,可能会创建虚假共享。在锁所有者有机会写入之前翻转缓存行为“共享”或无效化确实会减慢速度。
或者行中的空间可能是程序中需要存在的某些非常罕见的东西,但可能仅用于错误处理,因此其性能并不重要。如果它不能与互斥量共享一行,则必须在其他地方占用空间。(也许在某个“冷”数据页面上,所以这不是一个很好的例子)。 你可能不太可能想要malloc或new一个互斥锁本身,尽管它可以是你动态分配的类的一部分。分配器开销是真实存在的,例如使用16字节的内存作为分配前的簿记空间。(使用glibc的malloc/new进行大型分配通常是页面对齐+ 16字节,使它们与所有更宽的边界不对齐)。动态分配器簿记是互斥锁共享空间的非常好的选择:在使用互斥锁时可能没有任何读写操作。

非无锁的std::atomic对象通常使用一组锁(可能只是简单的自旋锁,但也可能是std::mutex)。如果是后者,你不希望两个相邻的互斥锁同时被使用,因此将它们全部打包在一起是很好的。


增加互斥锁的大小,是防止假共享的非常笨重的方法。如果一个实现想要确保 std::mutex 有一个自己的缓存行,那么它需要用 alignas(64) 声明它,以确保它的 alignof() 是这样的。这将强制填充使 sizeof(mutex) 成为 alignof 的倍数(在本例中相等)。

但请注意,如果你要固定一个大小,std::hardware_destructive_interference_size 在一些现代 x86-64 上应该是 128,因为 Intel 的 L2 缓存使用相邻线硬件预取。这是比同一缓存行更弱的破坏效应,而且这浪费了太多的空间。


接受了这个答案,因为它提供了许多好的观点,但是对于“平均”程序来说,我显然不确定这是否是STL实现者的正确选择,但是再次强调,对于我或任何人来说,无法对地球上的每个C++程序进行分析。 :) - NoSenseEtAl
@NoSenseEtAl:我不知道,我没有安装MSVC或Windows开发环境。我会假设没有有意的填充,但是我不知道他们会用那么多空间来做什么。可能包括一些内核资源的句柄,但是我不知道还有什么。 - Peter Cordes
STL在Github上,但如果没有VS,你就无法点击浏览,所以很难导航...我不是说你应该这样做,或者80证明我是对的,但你知道MSVC显然选择了大小而不是64,可能是因为虚假共享而故意这样做...但无从得知,可能只是一些需要保持互斥状态的大型东西。 - NoSenseEtAl
1
@NoSenseEtAl:我猜你现在意识到互斥锁有许多其他用例,因此为这些其他用例浪费空间将是权衡,以改善像你描述的情况的性能(仅当这种代码没有采取预防措施避免互斥锁的虚假共享时)。我认为声明一堆互斥锁并不是很常见,而且我认为互斥锁数组可能更加罕见。 - Peter Cordes
我只是不知道,就像我在第一条评论中所说的一样:“但是对于我或任何人来说,对地球上的每个C ++程序进行分析是不可能的”。 但是我认为,除非Dinkumware / MSFT的某个人在这里告诉我们他们为什么这样做,否则我们永远不会得到比你更好的答案。 :) - NoSenseEtAl
显示剩余4条评论

2
也许您的解决方案是使用alignas?类似于:
alignas(std::hardware_destructive_interference_size) std::mutex mut;

现在你的互斥锁位于硬件边界上。

  1. 我知道这个,我的问题是为什么STL实现不防范这种情况,因为我认为大多数人不知道它。
  2. hardware_destructive_interference_size在gcc/clang上仍未实现,可能会一直保持这样。https://stackoverflow.com/questions/62025586/what-is-the-reason-why-clang-and-gcc-do-not-implement-stdhardware-constructiv
- NoSenseEtAl

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