伪共享¶
什么是伪共享¶
伪共享与缓存机制有关,用于维护存储在多个 CPU 缓存中的一个缓存行的数据一致性;其学术定义见[1]。考虑一个包含引用计数和字符串的结构体
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 频繁地递增,而 ‘name’ 被其他 CPU 读取,由于 “共享”,所有读取 CPU 都必须一遍又一遍地重新加载整个缓存行,即使 ‘name’ 从未更改。
有很多现实世界中因伪共享导致性能下降的案例。其中之一是 rw_semaphore ‘mmap_lock’ 在 mm_struct 结构体中,其缓存行布局的改变引发了性能下降,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
在测试期间运行上述命令将-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”)
尽可能用 ‘read’ 替换 ‘write’,尤其是在循环中。例如,对于一些全局变量,使用 compare(read)-then-write 代替无条件写入。例如,使用
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 data + global data’,或合理地增加将 per-cpu 数据同步到全局数据的阈值,以减少或推迟对该全局数据的 ‘write’。
提交 520f897a3554 (“ext4: use percpu_counters for extent_status cache hits/misses”)
提交 56f3547bfa4d (“mm: adjust vm_committed_as_batch according to vm overcommit policy”)
当然,应仔细验证所有缓解措施,以避免产生副作用。为了在编码时避免引入伪共享,最好
注意缓存行边界
将大多数只读字段组合在一起
将同时写入的内容组合在一起
将经常读取和经常写入的字段分离到不同的缓存行中。
最好添加注释说明伪共享的考虑。
需要注意的是,有时即使在检测到并解决了严重的伪共享之后,性能也可能没有明显的改善,因为热点会转移到新的位置。
其他¶
一个悬而未决的问题是,内核有一个可选的数据结构随机化机制,该机制也会随机化数据成员之间缓存行共享的情况。