6

在gcc 和 clang 主干中跟随 static_assert传递。

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

由于 x86 CPU 有 64 字节的高速缓存行,我希望 mutex sizeof 为 64,因此可以避免错误共享。大小“只有”40字节有什么原因吗?

注意:我知道大小也会影响性能,但程序中很少有大量互斥体,因此与错误共享的成本相比,大小开销似乎可以忽略不计。

注意:有一个类似的问题问为什么 std::mutex 这么大,我在问为什么这么小:)

编辑:MSVC 16.7 的大小为 80。

4

2 回答 2

5

在不需要的地方强制填充将是糟糕的设计。如果用户没有任何有用的东西可以放入缓存行的其余部分,他们总是可以填充。

如果通常很少争用,您可能希望它与它所保护的数据位于同一缓存行中;在获取锁后访问共享数据时,只有一个缓存行可以反弹,而不是第二个缓存未命中。这可能在细粒度锁定中很常见,在这种锁定中许多对象都有自己的std::mutex,并且使其保持较小更有益。

(激烈争辩可能会在试图获取锁的读者与获得锁所有权后写入共享数据的锁所有者之间创建虚假共享。在锁所有者有机会之前将缓存行翻转为“共享”或无效写,确实会减慢速度)。


或者该行其余部分的空间可能是一些非常罕见的东西,需要存在于程序中的某个位置,但可能仅用于错误处理,因此其性能无关紧要。如果它不能与互斥锁共享一行,它就必须在其他地方占用空间。(也许在某些“冷”数据页面中,所以这不是一个很好的例子)。

您可能不太可能想要mallocnew互斥体本身,尽管它可能是您动态分配的类的一部分。分配器开销是真实存在的,例如在分配簿记空间之前使用 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 缓存中的相邻线硬件预取。这比相同的高速缓存行具有更弱的破坏性效果,而且浪费的空间太多。

于 2020-10-02T11:33:53.677 回答
2

也许您的解决方案是使用对齐?就像是

alignas(std::hardware_destructive_interference_size) std::mutex mut;

现在您的互斥体位于硬件边界上。

于 2020-10-02T13:44:53.950 回答