今日是周末,上海的天气陡然降至冬日的感觉,恰好小朋友学校春游了。正好宅在家里一整天,码了此文。
SPE简介
Arm Statical Profiling Extension (SPE)是从armv8.2开始引入的,它提供了硬件上的统计采样的支持。
我们已经有了PMU为什么还要SPE呢?
PMU可以通过累计计数event(如cache miss)的方式获取代码执行过程中总的event数量。从而从总体宏观上得到应用/系统运行过程的发生的event(如cache miss)的信息。利用perf tool的’perf stat’可以获取这些信息。
perf stat -e l1d_cache_refill,l1d_cache ./false_sharing -n 1000000000 -t 0 -c 5 6
Performance counter stats for './false_sharing -n 1000000000 -t 0 -c 5 6':
85,646,174 l1d_cache_refill
4,012,218,750 l1d_cache
其工作方式为:
PMU也可以实现当设置观测的event如cache miss到达设置的数量(count number)后产生PMU中断,通过中断异常的ELR/LR获取这是被中断指令的地址(即被采样点),在PMU中断处理中,产生perf event record放到perf ring buffer中。利用这样的周期性采样获取的信息,可以大致从统计学意义上分析性能,如代码热点和那些代码导致了那些event(如cache miss/TLB miss/branch mispredication)。利用perf tool的’perf record‘可以获取这些信息,利用’perf report’可以分析这些信息。
一个例子:
通过perf record命令来收集perf数据:
perf record -c 5000 -e l1d_cache_refill,l1d_cache ./false_sharing -n 1000000000 -t 0 -c 5 6
通过perf report来分析收集的perf数据:
#
# Samples: 19K of event 'l1d_cache_refill'
# Event count (approx.): 97580000
#
# Overhead Command Shared Object Symbol
# ........ ............. ................. .............................
#
50.01% T0 false_sharing [.] 0x0000000000000eb4
49.78% T1 false_sharing [.] 0x0000000000000f1c
0.13% T1 false_sharing [.] 0x0000000000000f18
0.05% T1 false_sharing [.] 0x0000000000000f28
0.01% T1 [kernel.kallsyms] [k] __do_softirq
0.01% T0 false_sharing [.] 0x0000000000000eb8
0.01% T0 false_sharing [.] 0x0000000000000ec0
0.01% false_sharing [kernel.kallsyms] [k] PageHuge
0.01% false_sharing [kernel.kallsyms] [k] armv8pmu_set_event_filter
0.01% false_sharing [kernel.kallsyms] [k] perf_iterate_ctx
# Samples: 837K of event 'l1d_cache'
# Event count (approx.): 4186685000
#
# Overhead Command Shared Object Symbol
# ........ ............. ..................... .......................................
#
50.24% T1 false_sharing [.] 0x0000000000000f1c
49.71% T0 false_sharing [.] 0x0000000000000eb4
0.01% T1 false_sharing [.] 0x0000000000000f18
0.01% T0 false_sharing [.] 0x0000000000000eb0
0.00% T1 false_sharing [.] 0x0000000000000f28
PMU局限性
Perf record采样模式需要利用PMU中断,这可能导致采样位置票移的问题(获取到的被采样指令地址可能在真实触发event count减到0的时刻附近)。因为它利用的是中断服务程序收集采样触发位置。因为现代superscaler CPU从中断源产生中断到它被pipeline检测到可能需要一些cycles, 汇报到中断异常时ELR/lr的地址可能是同时issue指令的某一条指令地址。虽然,从函数级来分析,这些偏移可能没太多影响。但是,如果要将event归结于汇编指令级的某一条代码,可能会出现一些不容易理解的地方。
例如上面的perf record/report例子:
实际上false sharing的主要代码(反汇编)为:f18: f94002c0 ldr x0, [x22] f1c: 91000421 add x1, x1, #0x1 f20: 91000400 add x0, x0, #0x1 f24: f90002c0 str x0, [x22] f28: eb02003f cmp x1, x2 f2c: 54ffff61 b.ne f18 <false_sharing_test+0x238> // b.any eb0: f94002a0 ldr x0, [x21] eb4: 91000421 add x1, x1, #0x1 eb8: 91000400 add x0, x0, #0x1 ebc: f90002a0 str x0, [x21] ec0: eb02003f cmp x1, x2 ec4: 54ffff61 b.ne eb0 <false_sharing_test+0x1d0> // b.any
但我们看到上面perf annotate,sample到的PC是:
eb4这个地址对应的指令是add x1, x1, #0x1,他是eb0: f94002a0 ldr x0, [x21]下面的一条指令。后面我们可以看到利用SPE的profiling不会出现这样的问题,它是:
- 如果event count已经减计数到0,但是这个时候中断被mask了,这可能就会导致一些盲点/错误的热点。因为PMU中断无法被立刻响应,需要延迟到中断被软件enable才能响应,而这时汇报的ELR/lr已经不是真实触发event count减到0的时刻的代码地址:
AArch64 Linux的perf子系统依赖于PMU overflow中断。通过编程cycle counter在一定时间间隔overflow, 每个overflow中断都会被传送给CPU来进行代码路径的分析。Perf处理这些中断并采样这些运行进程的PC (program counter). 当仅有单个中断屏蔽可以给程序员控制时,任何在禁止中断状态下执行的critical section都无法被采样分析。用户通常抱怨的问题是他们的系统分析结果显示很多的是CPU时间都花在了local_irq_enable()上。这显然是不对的,因为这个函数是通过单条操作PSTATE.I 标志指令来实现的。这个误导信息是由PMU中断总是在IRQ重新被使能时传送给CPU的,perf总是采样到这个函数的PC。
Arm 2021构架扩展会引入硬件NMI的支持来解决这个问题。可以参考深入理解 Arm A-profile的non-maskable interrupt -NMI https://aijishu.com/a/1060000... PMU虽然能提供一段代码执行过程中的event信息,但是它不能精确地汇报某一条指令在执行过程的信息,如这条指令执行过程:
* 有没有导致cache miss, * 指令执行的latency, * 分支预测有没有预测成功,跳转是taken还是没有taken,预测的地址是什么 * load/store指令访问的内存虚拟地址和物理地址是什么… , * load/store指令的数据源来在那一级cache/同一cluster的CPU/另外一个Cluster的CPU/另一个socket的CPU
这些信息在性能分析/debug的时候可能会有帮助。
一个分析branch predict miss的例子:
for(i=0;i<TEST_IRT;i++)
{
if (__builtin_expect(test_data[i]==1,0))
val=val*(i+2);
else
val=val*(i+3);
}
test_data里面是一些随机的0,1。接下来通过perf record -e branch-misses 采样,然后通过perf annotate来分析,得到的结果是:
从这个branch miss采样点的比例,比较难看出问题,也不知道实际上对应那条跳转指令。
- PMU的方式需要产生中断->进入Linux kernel中断处理过程->调用PMU driver->perf driver将record通过软件写入buffer。这些过程是比较耗时耗CPU执行cycle的。为了避免对正常应用/系统的性能影响,这种方式不能将采样频率设置太高。
Arm SPE
Arm SPE可以通过硬件采集CPU pipeline执行过程中的发生的event, 然后根据软件设置的count number周期性地(当采集的event数量达到count number,即count down到0时)采样这时这条指令的执行信息,并由硬件产生SPE的record packet, 并由硬件将packet写入外部内存。你可以简单理解为SPE将之前PMU软件需要做的事情硬件化实现了(SPE能记录的event种类一般会少于PMU event,但也会有一些PMU没有的信息)。
由上面介绍的SPE工作方式可知,SPE采样的指令地址信息是非常精确的,不会漂移。而且采样记录的开销较小,因而采样率可以设置的很高(当然也是有限制的,不会到每条指令都被采样的精度)。
构架上,SPE的采样可以在指令级的,也可是micro operation (uop,一条指令可能在pipeline 里拆分为多个uop,多条指令也可以在pipeline中fuse成一个uop)级别的。Arm Neoverse N1 CPU SPE采样是uop级别的。为了表示简单,下面内容基本不区分指令级和uop级。
SPE还可以提供被采样指令的额外信息,如:
- 指令本身的地址
- 访问数据的虚拟和物理地址
- 指令Issue latency和总latency
- 事件(如cache miss)
- 时间戳(timestamp)
- 这些信息让开发人员获得非常详细的代码执行信息。
SPE工作过程
利用SPE可以分成4个阶段:
- 以一个可编程的采样间隔从采样总体中选择一个操作(可以限定为某个EL)。这个过程可以加入一些随机,伪随机或扰动到采样间隔中。
- 硬件跟踪这个操作在pipeline执行信息,包括PC,事件,时序,数据地址。这既是profiling operation。
在硬件创建一个采样记录之前,可以设置过滤由step 2产生的profiling operation, 可设置的过滤条件可以是:
a. Operation的类型 b. 事件 c. Latency
- 如果这个采样没有被step3过滤掉,硬件会创建一个采样记录,它包含step 2的信息。然后这个采样记录会生成一个标准的数据record,并被硬件写入外部内存buffer。
SPE将采样记录序列写入内存,每个采样记录包含一个packet序列,每个包包含:
- 1或2个字节的header
- 0,1, 2, 4或8个字节的payload
下面是一个原始数据记录的例子:
如果要获取包含PID的context packet,我们需要在kernel配置中使能CONFIG_PID_IN_CONTEXTIDR
SPE packet
Note:
- 记录PC的address packet包含采样的PC virtual address, EL level和安全状态。
- Branch操作可能会产生一个记录Branch target address的address packet
- Data source Packet是CPU实现相关的(Implementation Dependent)
Latency信息
SPE可以记录以下被采样操作的latency信息:
- Issue latency:此操作从被dispatch到被issue出去执行之间的cycle数。它计数操作在issue阶段的延迟(可能因为后端structure hazard,等待input operand ready等原因)
- Total latency: 此操作被dispatch到被执行完成之间的cycle数。
- Translation latency: 此操作从虚拟地址送到MMU到MMU地址翻译完成之间的cycle数。一般用于load/store操作访问的内存地址的MMU翻译过程。
Data Source信息
Data Source信息用于记录一个load操作访问的数据的来源。利用此信息可以分析数据访问来自不同的数据源的延迟,还可以分析实现cache一致性时CPU cache之间snoop的情况从而进一步分析false sharing,或优化数据访问避免相距较远的CPU之间的snoop导致的大延迟。
Data Source的解释和具体的CPU/SoC设计相关,以N1 CPU为例:
Event Packet
SPE构架支持的event分为:
- 构架要求支持的event
- CPU实现相关的event
以load/store操作和branch操作可以支持的event为例:
Load/store操作:
Branch操作:
Linux上如何使用SPE
使能SPE
- 使能CONFIG_ARM_SPE_PMU kernel选项
- 禁止kpti, 通过加上”kpti=off”选项
查看 /sys/bus/event_source/devices/arm_spe_0是否存在,说明spe_pmu driver工作正常。然后通过”perf list | grep arm_spe”查看一下perf tools是否支持spe.
通过perf利用SPE
可以通过以下步骤利用SPE profiling:
获取包含spe的perf data
perf record -e arm_spe_0/branch_filter=1,load_filter=1,store_filter=1,ts_enable=1,pct_enable=1,pa_enable=1,min_latency=10,jitter=1/ -- ./user_app
可以使用的option包括
需要注意的是:设置这些filter不影响上面介绍SPE ‘Sample Population’和’ Sample is taken’过程,只影响最终写入到SPE buffer的record. 例如对于同一个应用,假设应用执行的代码和数据访问不会改变,通过
perf record -e arm_spe_0/load_filter=1,store_filter=1/ -- ./user_app
perf record -e arm_spe_0/load_filter=1/ -- ./user_app
得到的SPE record中的load操作记录是一样。
还可以通过加上—all-kernel或是—all-user设置特定的EL。通过-c xxx选项指定采样间隔(sample interval).
分析spe perf data
Perf report -D -i ./perf.data > spe_report.txt
上面命令导出解密过的spe packet的文本。
Perf report –stdio -i ./perf.data > spe_report.txt
上面命令导出具有更多分析信息的report文本。
Perf mem
‘perf mem’用于获取更详细内存访问信息,在arm上是基于SPE实现的。
Arm上没有和x86一样的直接mem-loads和mem-stores event,但SPE通过适当设置可以得到类似的信息:
perf mem report
Perf c2c工具
Perf c2c工具可以用来检测cache line的false sharing的场景。在arm上也可以利用SPE来获取所需要的信息。
当多个Core同时读写同一个内存地址时,需要通过Cache Coherence硬件机制完成一致性,会产生额外的性能开销。这种情况是真正的“共享”。
当多个Core同时读写多个不同的地址,但这些地址由于相邻会被映射到同一个Cache Line时,虽然从程序逻辑上不存在内存地址“共享”,但因为Cache实现机制的问题存在一个隐式的“共享”,我们称之为False Sharing。
Linux kernel v6.0开始支持arm上的perf c2c. arm上不能使用x86的HITM event,取而代之的是SPE记录中的data source, data address和PC。
Arm上使用’SNOOP_PEER’来指示发生在不同CPU间的snoop。我们需要检查’peer snoop’指标来确定false sharing的问题。
Linaro上有一篇blog值得一看:Using the Arm Statistical Profiling Extension to detect false cache-line sharing https://www.linaro.org/blog/u...
Leo Yan也在Linaro会议上分享了LVC21-302: Integration Arm SPE in Perf for Memory Profiling: https://resources.linaro.org/...
Arm streamline对SPE的支持
Arm streamline工具已经支持SPE,可以利用SPE数据进行系统的profiling。
已经有一篇blog详细介绍了streamline如何使用SPE进行性能分析,这里就不赘述。
https://community.arm.com/arm...
SPE parser
虽然perf report提供了比较好已经分析好的信息。但有的时候,性能分析人员可能需要自己根据分析需求把原始的SPE record进行处理。
Arm也提供了一个python程序SPE parser,它可以将perf.data转换为cvs或parquet格式
https://gitlab.arm.com/teleme...
PMU vs SPE profiling
那么有了SPE profiling是不是可以完全替代PMU profiling呢?
其实不是,PMU和SPE各有所偏重。
- PMU有SPE不能提供的全局event计数,比如对整个应用过程的cache miss计数
- PMU的event采集方式和SPE不一样。PMU采集一段时间/代码内指令发生的某些event的累计计数。而SPE是达到设定数量(sample interval)操作(load/store+branch)数时采样一个操作发生的event(如这个操作导致的cache miss),SPE更偏向于收集某单一操作的信息。
- PMU有SPE没有的event计数:例如strex pass/fail,执行的指令数量/类型等
- SPE能提供PMU没有的单条指令执行信息,PMU更多是许多条指令的累计信息。
可以做一个比喻:一家工厂产线上有很多工人,为了观测效率,可以有不同的方式:
1. 每星期获得工人们总的生产产品的数量和总工时。公司老板可能只需要关心一年的产品数量。
2. 当产品每增加到一定数量之后,开始抽查其中一个工人一天的工作情况,什么时候上班,什么时候下班,有没有偷懒。利用这些信息获知工人们的工作效率。但有可能有些工人有概率不会被抽查到。
方式1类似于PMU,方式2类似于SPE。方式1可以得到更宏观一些的信息,而SPE可以获取更微观一些的信息。
因此性能分析时应该采用PMU和SPE结合的方式。有时PMU和SPE可能会给出不一样甚至有些相悖的信息也可以理解。
让我们在利用false sharing这个例子,但我们在perf record的时候同时使用PMU和SPE:
perf record -e l1d_cache_refill,l1d_cache,arm_spe_0/ts_enable=1,load_filter=1,branch_filter=1,store_filter=1,min_latency=1,jitter=1/ -- ./false_sharing -n 100000000 -t 0 -c 5 6
在通过perf report来看看:
通过PMU的分析为:
# Total Lost Samples: 0
#
# Samples: 7K of event 'l1d_cache_refill'
# Event count (approx.): 9156492
#
# Children Self Command Shared Object Symbol
# ........ ........ ............. ................. ..............................
#
49.83% 49.83% T1 false_sharing [.] 0x0000000000000f1c
49.81% 49.81% T0 false_sharing [.] 0x0000000000000eb4
0.20% 0.20% false_sharing [kernel.kallsyms] [k] perf_event_alloc
0.08% 0.08% false_sharing [kernel.kallsyms] [k] kfree
0.02% 0.02% T1 [kernel.kallsyms] [k] arch_local_irq_enable
0.01% 0.01% T0 [kernel.kallsyms] [k] arch_local_irq_enable
0.01% 0.01% T0 false_sharing [.] 0x0000000000000eb0
0.00% 0.00% false_sharing [kernel.kallsyms] [k] handle_mm_fault
# Samples: 7K of event 'l1d_cache'
# Event count (approx.): 404996532
#
# Children Self Command Shared Object Symbol
# ........ ........ ............. ..................... .............................
#
49.94% 49.94% T0 false_sharing [.] 0x0000000000000eb4
49.93% 49.93% T1 false_sharing [.] 0x0000000000000f1c
0.03% 0.03% false_sharing [kernel.kallsyms] [k] vm_unmapped_area
0.03% 0.03% false_sharing [kernel.kallsyms] [k] next_uptodate_page
0.03% 0.03% false_sharing [kernel.kallsyms] [k] unmap_page_range
0.01% 0.01% T0 false_sharing [.] 0x0000000000000eb0
0.01% 0.01% false_sharing ld-linux-aarch64.so.1 [.] 0x0000000000009dec
0.01% 0.01% T1 false_sharing [.] 0x0000000000000f18
0.01% 0.01% T1 false_sharing [.] 0x0000000000000f24
0.00% 0.00% false_sharing [kernel.kallsyms] [k] inode_permission
0.00% 0.00% T0 [kernel.kallsyms] [k] __handle_mm_fault
而通过SPE得到的分析为:
# Samples: 2K of event 'l1d-miss'
# Event count (approx.): 2666
#
# Children Self Command Shared Object Symbol
# ........ ........ ............. ..................... .......................................
#
53.30% 53.30% false_sharing false_sharing [.] 0x0000000000000f18
46.49% 46.49% false_sharing false_sharing [.] 0x0000000000000eb0
0.60% 0.60% false_sharing [kernel.kallsyms] [k] perf_iterate_ctx
0.53% 0.53% false_sharing [kernel.kallsyms] [k] perf_event_task_tick
0.19% 0.19% false_sharing [kernel.kallsyms] [k] perf_event_wakeup
0.11% 0.11% false_sharing [kernel.kallsyms] [k] perf_event_alloc
0.08% 0.08% false_sharing [kernel.kallsyms] [k] check_preempt_curr
0.08% 0.08% false_sharing [kernel.kallsyms] [k] ctx_sched_out
0.08% 0.08% false_sharing [kernel.kallsyms] [k] event_sched_out
# Samples: 349K of event 'l1d-access'
# Event count (approx.): 349656
#
# Children Self Command Shared Object Symbol
# ........ ........ ............. ..................... ..........................................
#
65.61% 65.61% false_sharing false_sharing [.] 0x0000000000000f18
32.11% 32.11% false_sharing false_sharing [.] 0x0000000000000eb0
0.26% 0.26% false_sharing [kernel.kallsyms] [k] perf_output_copy
0.18% 0.18% false_sharing [kernel.kallsyms] [k] sched_clock
0.15% 0.15% false_sharing [kernel.kallsyms] [k] armv8pmu_handle_irq
PMU和SPE的分析结果有很大的差异:
PMU的分析两个CPU/Thread采样到的L1 cache refill/access是很接近的。而且除了采样对测试变量的LDR指令之外(虽然PC偏移了4 byte,实际应该为0x0f18和0x0eb0,有0.01%采样到PC准确为0x0b0和0x0f18),它还采样到少部分对测试变量的STR指令
0.01% 0.01% T1 false_sharing [.] 0x0000000000000f24
SPE的分析两个CPU/Thread采样到的L1 cache refill/access相差较大,而且只采样到对测试变量的LDR指令。通过SPE parser分析每条SPE record也可以确认的确没采样到对测试变量的STR指令。
为了分析,我在再使用perf stat -e l1d_cache_refill,l1d_cache --cpu 来获取每个CPU的L1 cache refill/access的统计/累计数量:
perf stat --cpu 5 -e l1d_cache_refill,l1d_cache ./false_sharing -n 100000000 -t 0 -c 5 6
Performance counter stats for 'CPU(s) 5':
4,600,775 l1d_cache_refill
200,844,437 l1d_cache
perf stat --cpu 6 -e l1d_cache_refill,l1d_cache ./false_sharing -n 100000000 -t 0 -c 5 6
Performance counter stats for 'CPU(s) 6':
5,340,258 l1d_cache_refill
200,885,890 l1d_cache
由此看见,两个CPU/Thread采样到的L1 cache refill/access应该是比较相近的。
可能的解释是:
cache refill可能发生在改这个变量的LDR指令上,也可能在STR指令上。但SPE只采样到了LDR指令,而SPE的分析是只基于这些LDR指令发生的cache refill/access进行统计,可能导致偏差。
但PMU不存在这样的问题,因为即使是绝大多数采样到的也是LDR指令,但是PMU的cache miss/TLB miss的信息已经包含了上次采样到本次采样之间(包括LDR+STR)发生的这些event.
需要注意的是:N1 CPU的某些版本SPE有点小问题:SPE硬件采样到STR指令时,SPE record不会包含STR指令的cache miss/TLB miss event packet.
从这个例子可以看出PMU分析可以给出给宏观的分析结果,然后可以进一步利用SPE的data source这样独用的信息分析出是因为false sharing导致的性能降低。
参考
Integration Arm SPE in Perf for Memory Profiling: https://resources.linaro.org/...