28

修志龙_ZenonXiu · 2023年03月19日 · 上海市

探讨LSE Atomic和LDREX/STREX 实现atomic的Scalability

前文介绍了通过Load-Exclusive/Store-Exclusive (Load-Linked/Store-Condition, LLSC)和LSE atomic机制实现的锁公平性问题。
https://aijishu.com/a/1060000...
本文探讨这两种机制实现的atomic访问的可伸缩性(scalability)问题。

作为Armv8.1-A引入的LSE(Large System Extension)扩展的一部分,LSE Atomic指令应该在大系统中的表现比LLSC机制更加有效。我试图通过一个实际的系统来研究一下这两种机制。

Ampere Altra平台

通过运行Linux上的lstopo命令,我们可以得到这个平台的一些信息。
cpu_topo.png

Ampere Altra是比较合适这个研究的硬件平台。我们使用的Ampere Altra是基于32个Neoverse N1核,通过CMN mesh网络连接的。

可知每个DSU cluster中包含两个N1核,此平台上总共有32核。
再通过cat /sys/kernel/debug/arm-cmn/map 命令,可以显示出CMN的mesh结构:
mesh.JPG

我将它的mesh转成为下图
mesh.jpg

可知CMN mesh网络是6x8的mesh,而这些DSU Cluster是连接到mesh中RN-F node上的。

测试设计

我设计了一个测试:创建多个线程,并将每个线程绑定到不同的CPU核上运行。设计一个全局count变量,这些线程完成的工作是对count进行atomic性的(原子操作)加N次1的操作:
1, 对于LLSC方式的测试,它使用LDREX,STREX 指令对全局count加1, 大致的伪代码为:

for (i=0; i<N;i++)
{
   asm (
          L1:
             ldrex  value, [&count]
             add    value, value, #1
             strex   ret, value, [&count]
             cbnz   ret, L1
           )
}

2,对于LSE atomic方式的测试,它使用STADD指令对全局count加1, 大致的伪代码为:

mov x2, #1
for (i=0; i<N;i++)
{
   asm (
           stadd x2, [&count]
          )
}

对于1和2测试,生成两个不同的测试程序,我们称之为atomic_llsc_test和atomic_lse_test。这个测试程序可以输入需要运行测试的CPU核数(也即是线程数)和N的值,从而得到不同核数下对count加的耗费时间和速率的数据。考虑到Linux系统中CPU 0一般需要运行很多系统代码,为了避免这个因素带来的影响,我选择从CPU 5开始运行测试程序。每个线程最终对count加N,所有的线程对count最终加 Nnumof_threads*N。

我还通过trace-cmd+kernelshark, 和perf来观测运行情况和获取一些PMU event数据进行分析。

测试结果

测试耗费时间

Picture1.png
我选择了测试1, 2,4,6,..24个线程/核进行测试,得到运行完测试程序中所有线程最终耗费时间。上图中X坐标为线程数,Y坐标为耗费的时间(越小越好)。

随着核数的增加(相应最终对count加的次数也会增加,N*num_of_threads),耗费的时间也会增加,这是正常的,因为不管使用LSE atomic还是LLSC方式,多核间竞争count的开销都是存在的。我们感兴趣的是这个开销对于LSE atomic和LLSC这两个方式的影响趋势。
由上图可以看出,使用LSE Atomic的方式,其影响相对线性,而LLSC的方式随着核的数量增加,明显越来越差。

为了更好理解,我利用了另一个角度来分析,平均加的次数(rate)。获取数据的方式是:当一个线程结束时,将这时的count值除以‘测试程序从开始运行这些线程到此刻的时间差’。因为每个线程结束的时间不一样,因而会得到不同的rate。
从我的观测看到,对于LSE Atomic方式,不同线程的rate比较接近,因为线程们的结束时间比较接近,对于LLSC方式,随着核数的增加,不同线程的rate会拉开,24核测试时rate差别极大。

以24核测试为例,通过kernelshark来观测,
LSE Atomic方式的表现为:

kernelshark_24c_lse_nop_1.JPG

而LLSC方式的表现为:
kernelshark_24c_llsc_nop_1.JPG

从图中可以看到,
1. 总的运行时间,LSE Atomic为 4.65秒左右,而LLSC的运行时间为50.77秒左右
2. LSE Atomic方式中不同线程的运行时间相对接近,而LLSC方式中,Thread7-14 (运行在CPU13-20上)明显比其他线程快很多。这样体现了前文所探讨的锁公平性问题。

Rate

下面给出得到的rate数据:
Picture1.jpg

由次可知,总的来说,LSE Atomic方式比LLSC方式的rate更好,而且随着核的数量增加,rate相对稳定。
注意:考虑到LLSC不同线程完成时算的rate有差异,LLSC有LLSC_1, LLSC_2两条曲线,可以认为LLSC的rate在这两条曲线之间。而LSE Atomic方式不同线程完成时算的rate差别不大,因而只有一条曲线。

两个方式从1核到2核的rate降得都比较厉害,因为从1核的无竞争到2核的多核竞争。

Perf分析

通过perf和PMU event count来获取不同核数测试中其中两个CPU核(CPUn, CPUm)的L1 Data Cache Refill(因为L1 Data Cache Miss导致的refill),和STREX PASS/STREX Fail数量。

得到的L1 Data Cache Refill结果为:
Picture3.png

可知,总的来说LSE Atomic方式的L1 Data Cache Refill相对较少,而且LLSC方式当核数增加时,如上面kernelshark显示,有些线程会得到更多机会,从而比其他线程快的多地完成任务,上面的L1 Data Cache Refill反映了这点,LLSC方式有些核/线程随着核数达到一定数量,L1 Data Cache Refill数量反而下降了。这个也可以从前文锁的公平性上获得一定解释。

得到的STREX PASS/STREX Fail数量结果为:
因为LSE Atomic方式不采用LDREX/STREX,这个数据只对LLSC有意义。
每个线程结束时,其运行的CPU的STREX PASS计数为N,因而不需要特别分析,只分析STREX Fail,
Picture6.png

由此可知,总的来说,随着核/线程数的增加,STREX Fail数量会增加,但是在核数比较多时,也会有一些CPU/线程的STREX Fail数量反而会减少(因为其获得更多的机会)。

结论

在一些有很多核的大型系统/平台上,对于很频繁使用的多核共享的锁和atomic操作,LSE Atomic方式有比LLSC方式有更好的性能和公平性,更加scalable。

推荐阅读
关注数
8629
内容数
51
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息