变量独立,缓存行却相同
伪共享的英文名称是False sharing。“伪”指程序逻辑上不必共享同一变量,硬件却把相邻变量当作同一缓存一致性单元处理。假设某机器的缓存行是64字节,两个独立计数器恰好位于同一行,线程甲在一个核心反复写第一个,线程乙在另一个核心反复写第二个。即使各自只负责自己的计数器,两者仍可能争用整条缓存行。
在采用写失效机制的模型中,一个核心写入前取得相应权限,会使其他核心持有的行副本失效;另一核心再写自己的计数器时,又需要取得该行。实际频繁的内存访问于是带来一致性消息和缓存间传递。64字节只是这个例子的设定,不能拿来代替目标处理器的实际参数。
与真共享、数据竞争和缓存冲突的区别
真共享是线程确实需要访问同一个逻辑数据,例如共同更新一个总计数器。伪共享则是不同数据因布局相邻而互相牵连;把相邻计数器分开可能消除这种干扰,却不能消除共同更新同一计数器所需的协调。
数据竞争属于程序语义和同步问题。以C++工作草案为例,它涉及潜在并发的冲突访问、至少一个非原子操作及缺少必要的先行关系。各线程访问不同内存位置并不因共处一行就自动形成数据竞争;反过来,使用原子变量保证语义正确,也未必消除行级争用。缓存冲突缺失又是另一机制:不同内存块映射到同一缓存组、超过可同时容纳的路数,造成替换,不要求多个线程写同一行。
怎样证明慢在这里
伪共享并不限于双方都写。Linux文档举出引用计数与只读名称同处一行的结构:一个核心频繁改计数,其他核心即使只读从未改变的名称,也可能被迫重新取得整行。若所有相关访问都只是读取,则没有这类由写入引起的失效往返。
但线程增加后变慢并不足以确诊。应先定位热点,再把缓存间传递的采样位置与成员偏移、访问指令对上。Linux的perf c2c可帮助寻找争用缓存行,pahole可展示结构布局。Intel手册特别提醒,HITM相关观测同时可能涉及真共享和伪共享;看到计数升高后,还须检查线程究竟在访问同一成员还是不同行内偏移。
布局与分工的取舍
可行的缓解方法包括把相互干扰的热字段分到不同行、按线程或处理器分区保存计数,再在适当时机汇总。经常一起写的数据与大多只读的数据,也可按实际访问模式重新组织。调整后的程序仍须保持原有同步语义,不能为了减少流量而删除必要的共享更新。
填充和对齐会增加对象体积,进而占用更多缓存、内存和地址转换资源;重排一个字段也可能使别的字段发生新的伪共享。应在相同工作负载下复测布局修改,而不是给每个小变量盲目填充。即使消除了明显的行争用,其他瓶颈也可能限制总性能。