伪共享¶
什么是伪共享¶
伪共享与缓存机制有关,该机制用于维护存储在多个 CPU 缓存中的同一个缓存行的数据一致性;其学术定义见于 [1]。考虑一个包含引用计数和一个字符串的 struct with
struct foo {
refcount_t refcount;
...
char name[16];
} ____cacheline_internodealigned_in_smp;
成员 “refcount”(A) 和 “name”(B) _共享_ 一个缓存行,如下所示
+-----------+ +-----------+
| CPU 0 | | CPU 1 |
+-----------+ +-----------+
/ |
/ |
V V
+----------------------+ +----------------------+
| A B | Cache 0 | A B | Cache 1
+----------------------+ +----------------------+
| |
---------------------------+------------------+-----------------------------
| |
+----------------------+
| |
+----------------------+
Main Memory | A B |
+----------------------+
“refcount” 会被频繁修改,而 “name” 仅在对象创建时设置一次且从不修改。当许多 CPU 同时访问 “foo” 时,“refcount” 经常被某个 CPU 递增,而其他 CPU 则在读取 “name”,由于这种 “共享”,所有这些读取的 CPU 不得不一遍又一遍地重新加载整个缓存行,尽管 “name” 从未改变过。
有许多由伪共享导致的性能退化的现实案例。其中之一是 mm_struct 结构体内部的 rw_semaphore “mmap_lock”,其缓存行布局的改变引发了性能退化,Linus 对此进行了分析,见 [2]。
有害的伪共享有两个关键因素
被许多 CPU 访问(共享)的全局数据
在对该数据的并发访问中,至少存在一个写操作:写/写或写/读的情况。
这种共享可能来自完全不相关的内核组件,或者同一内核组件的不同代码路径。
伪共享陷阱¶
过去,当一个平台只有几个 CPU 时,为了使热数据成员保持缓存热态并节省缓存行/TLB,可能会刻意将它们放在同一个缓存行中,比如锁以及受其保护的数据。但对于拥有数百个 CPU 的现代大型系统来说,当锁竞争激烈时,这可能就行不通了,因为持有锁的 CPU 可能会写入数据,而其他 CPU 则忙于在锁上自旋。
回顾过去的案例,伪共享有几种频繁出现的模式
锁(自旋锁/互斥锁/信号量)和受其保护的数据被刻意放在同一个缓存行中。
全局数据被放在同一个缓存行中。一些内核子系统有许多体积很小(4 字节)的全局参数,它们很容易被组合在一起并放入一个缓存行。
大型数据结构的数据成员在未被注意的情况下随机地靠在一起(缓存行通常为 64 字节或更大),例如 “mem_cgroup” 结构体。
接下来的“缓解”一节提供了实际的例子。
除非刻意检查,否则伪共享很容易发生;对于性能关键的工作负载,运行特定工具来检测影响性能的伪共享情况并进行相应的优化,是非常有价值的。
如何检测和分析伪共享¶
perf record/report/stat 被广泛用于性能调优,一旦检测到热点,就可以进一步使用 “perf-c2c” 和 “pahole” 等工具来检测和精确定位可能存在伪共享的数据结构。“addr2line” 在存在多层内联函数时解码指令指针也非常有用。
perf-c2c 可以捕获具有最多伪共享命中的缓存行、访问该缓存行的已解码函数(文件的行号)以及数据的内联偏移量。简单的命令如下
$ perf c2c record -ag sleep 3
$ perf c2c report --call-graph none -k vmlinux
在测试 will-it-scale 的 tlb_flush1 用例时运行上述命令,perf 会报告类似以下内容
Total records : 1658231
Locked Load/Store Operations : 89439
Load Operations : 623219
Load Local HITM : 92117
Load Remote HITM : 139
#----------------------------------------------------------------------
4 0 2374 0 0 0 0xff1100088366d880
#----------------------------------------------------------------------
0.00% 42.29% 0.00% 0.00% 0.00% 0x8 1 1 0xffffffff81373b7b 0 231 129 5312 64 [k] __mod_lruvec_page_state [kernel.vmlinux] memcontrol.h:752 1
0.00% 13.10% 0.00% 0.00% 0.00% 0x8 1 1 0xffffffff81374718 0 226 97 3551 64 [k] folio_lruvec_lock_irqsave [kernel.vmlinux] memcontrol.h:752 1
0.00% 11.20% 0.00% 0.00% 0.00% 0x8 1 1 0xffffffff812c29bf 0 170 136 555 64 [k] lru_add_fn [kernel.vmlinux] mm_inline.h:41 1
0.00% 7.62% 0.00% 0.00% 0.00% 0x8 1 1 0xffffffff812c3ec5 0 175 108 632 64 [k] release_pages [kernel.vmlinux] mm_inline.h:41 1
0.00% 23.29% 0.00% 0.00% 0.00% 0x10 1 1 0xffffffff81372d0a 0 234 279 1051 64 [k] __mod_memcg_lruvec_state [kernel.vmlinux] memcontrol.c:736 1
关于 perf-c2c 的一篇极好的介绍见 [3]。
“pahole” 按缓存行粒度解码数据结构布局。用户可以将 perf-c2c 输出中的偏移量与 pahole 的解码结果进行匹配,以定位准确的数据成员。对于全局数据,用户可以在 System.map 中搜索数据地址。
可能的缓解措施¶
并非总是需要缓解伪共享。伪共享的缓解措施应当平衡性能收益与复杂性及空间消耗。有时,较低的性能是可以接受的,没有必要对每个很少使用的数据结构或冷代码路径进行过度优化。
随着核心数量的增加,伪共享损害性能的情况越来越频繁地出现。由于这些不良影响,各个子系统(如网络和内存管理)中已经提出了许多补丁并被合并。一些常见的缓解措施(带示例)是
将热全局数据隔离在其自己的专用缓存行中,即使它只是一个 “short” 类型。缺点是会消耗更多的内存、缓存行和 TLB 条目。
提交 91b6d3256356 (“net: cache align tcp_memory_allocated, tcp_sockets_allocated”)
重新组织数据结构,将相互干扰的成员分隔到不同的缓存行。缺点之一是它可能会引入针对其他成员的新伪共享。
提交 802f1d522d5f (“mm: page_counter: re-layout structure to reduce false sharing”)
尽可能用 “读” 替换 “写”,特别是在循环中。例如对于某些全局变量,使用比较(读)后写入来代替无条件写入。例如,使用
if (!test_bit(XXX)) set_bit(XXX);而不是直接使用 “set_bit(XXX);”,对于 atomic_t 数据也是如此
if (atomic_read(XXX) == AAA) atomic_set(XXX, BBB);提交 7b1002f7cfe5 (“bcache: fixup
bcache_dev_sectors_dirty_add()multithreaded CPU false sharing”)提交 292648ac5cf1 (“mm: gup: allow FOLL_PIN to scale in SMP”)
尽可能将热全局数据转变为 “per-cpu 数据 + 全局数据”,或者合理提高将 per-cpu 数据同步到全局数据的阈值,以减少或推迟对该全局数据的 “写” 操作。
提交 520f897a3554 (“ext4: use percpu_counters for extent_status cache hits/misses”)
提交 56f3547bfa4d (“mm: adjust vm_committed_as_batch according to vm overcommit policy”)
当然,所有的缓解措施都应经过仔细验证以确保不会引发副作用。为了在编码时避免引入伪共享,最好
了解缓存行边界
将大多只读的字段归类到一起
将同时写入的事物归类到一起
将频繁读取和频繁写入的字段分隔在不同的缓存行上。
并且最好添加一条注释说明对伪共享的考量。
需要注意的是,有时即使检测并解决了严重的伪共享问题,由于热点转移到了新的地方,性能可能仍然没有明显的改善。
杂项¶
还有一个未决问题是,内核拥有一个可选的数据结构随机化机制,这也会打乱数据成员之间缓存行共享的情况。