当缓存行大小通常为 64 字节时,为什么 sizeof std::mutex == 40

Why is sizeof std::mutex == 40 when cache line size is often 64 bytes

在 gcc 和 clang 主干中都遵循 static_assert passes

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

因为 x86 CPU 有 64 字节的高速缓存行,所以我期望互斥量 sizeof 是 64,所以可以避免错误共享。 为什么大小“只有”40 字节是有原因的?

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

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

编辑:MSVC 16.7 的大小为 80。

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

您可能希望将它与它所保护的数据放在同一个缓存行中,如果它通常是轻微竞争的话;在获取锁后访问共享数据时,只有一个缓存行会反弹,而不是第二个缓存未命中。这可能与 fine-grained 锁定很常见,其中许多对象都有自己的 std::mutex,并且使它保持较小更有益。

(竞争激烈可能会在尝试获取锁的读者与锁拥有者在获得锁的所有权后写入共享数据之间创建虚假共享。在锁所有者有机会写,确实会减慢速度)。


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

您可能不太希望 mallocnew 互斥量本身,尽管其中一个可能是您动态分配的 class 的一部分。分配器开销是真实存在的,例如在分配簿记 space 之前使用 16 字节的内存。 (glibc 的 malloc/new 的大分配通常是 page-aligned + 16 字节,使它们与所有更宽的边界不对齐)。 Dynamic-allocator 簿记对于互斥锁与 共享 space 是一件非常好的事情:当互斥锁在使用时,它可能不会被任何东西读取或写入。


Non-lock-free std::atomic 对象通常 (可能只是简单的自旋锁,但 可能 是 std::mutex)。如果是后者,您不会期望 同时使用两个相邻的互斥量,因此最好将它们打包在一起。


此外,增加它的大小将是一种非常笨拙的方法来尝试确保没有虚假共享。 一个想要确保 std::mutex 有缓存的实现行到它自己这会想用 alignas(64) 声明它以确保它的 alignof() 是那个。这将强制填充使 sizeof(mutex) 成为 alignof 的倍数(在本例中等于)。

但是请注意 std::hardware_destructive_interference_size 在一些现代 x86-64 上应该是 128,如果你要为它固定一个大小,因为 adjacent-line 英特尔的二级缓存中的硬件预取。这比相同的 cache-line 的破坏性效果更弱,而且 space 浪费太多了。

也许您的解决方案是使用 alignas?像

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

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