仓库源文

假共享内存(False Sharing)


最近定位了一个问题,问题看来对很多写多线程的程序的工程师都有帮助,分享一下。

有一个多线程程序,业务分解为多个处理链路,Hash给每个线程去处理。但所有这些线程 由操作系统任意调度的时候,比都绑在同一个核上性能慢一倍。

开发工程师用perf跟踪了一下,发现完成一样的业务,所使用指令的数量是一样的,但IPC 的效率从0.53下降到了0.29。他们找不出原因让我们帮忙看看。

用perf record分析时间的瓶颈看不出什么问题,因为整个程序的函数时间分布很离散,最 忙的函数也不过占用2%的时间。所以我们用perf stat直接收集整个执行效果,收集到的信 息是这样的:

变化激烈的指标:::

    context-sw: 15k vs 11k
    cycle: 5b vs 2.7b (1.847GHz)
    bus_cycle: 5b vs 2b
    l1d-refill: 20.4m vs 18.7m, 7.4M/s vs 12M/s
    l1i-refill: 108m vs 86.4m, 39M/s vs 59M/s
    l2d-refill: 83.4m vs 37.6m
    l2i-refill: 45.9m vs 16.6m
    前端stall: 3b vs 1.4b
    后端stall: 1b vs 0.6b。
    branch miss: 28m vs 15.8m
    br_mis_pred: 28m vs 15m

基本不变的: ::

    inst: 1.5b vs 1.4b
    l1d_tlb: 800m vs 739m
    l1i_tlb: 18.9m vs 17.9m
    exec_ret/exec_taken: 164k vs 144k
    mem_access: 663m vs 650m
    pagefault: 0 vs 0

由这个情况推想,程序并没有因为调度到多个核上多走了分支,也没有多访问了内存,但 总线访问却大幅增加,看起来是因为这些线程间有很多“共享内存”访问,导致了很多的核 间Cache同步消息在bus上传播,(这不需要经过内存访问,参考:《内存访问模型》)从 而导致CPU在前后端执行上都发生了stall,最终拖慢了整个IPC(但为什么会导致branch miss上升我是没有想明白,可能和微架构的OoO队列使用有关吧)。

排查代码,发现代码中有很多这样的per thread数据结构:

unit32 per_channel_data[MAX_CHANNEL_NUM];

消除这个问题,代码就不再需要绑核才能提供足够的性能了。

通常,写OS代码的人不容易犯这种错误,因为他清楚知道自己的代码是多CPU的的,他用的 数据结构本来就是per_cpu的数据结构,自然会想到这是一种“共享内存”。但写应用程序的 工程师很容易忘掉这个问题,因为对他来说这只是不同的线程,而他的线程并没有共享这 些数据。

在今时今日,多线程基本上可以认为就是多CPU了,所以基本上,但凡这种per thread的数 据接口,就要考虑离得远远的,不然就变成核间的“共享内存”了,而结果我们也看见了, 性能可以100%这样下降的。

好吧,我承认,这个问题在鲲鹏920上会表现得更明显一点。因为鲲鹏使用多DIE设计,跨 DIE的MESI协议(Cache同步的协议)会造成更明显的影响。这几乎是用更多核来强化性能 的平台的通病,否则成本就得提高。所以,如果你对鲲鹏920做优化,尽量别让不同DIE上 的线程共享内存,否则你我都会很难受:)。

而且我认为这是未来有更多的核的时候,所有人都要面对的问题:你总不能要求核间效率 在你无限增加核的时候一直都能维持线性增长吧?