CPU那些事儿 - CPU benchmarks(上)

339 阅读15分钟

前言

本文重点关注SoC厂商具体用什么Benchmark做性能和功耗的分析。从笔者的经验来看,主要用到了 dhrystonecoremarklmbenchSPEC CPU2017Geekbench V6,我们会分析各个benchmark的特点和适用范围,会从源码和实践的角度。本文先分析 dhrystonecoremarklmbench。各个benchmark总体对比如下:

维度DhrystoneCoreMarkLMBenchSPEC CPU2017Geekbench
诞生年份/作者1984/Weicker2009/EEMBC组织1994/McVoy2017/SPEC组织2006/PrimateLabs组织
开源情况开源,C语言开源,C语言开源,C语言付费,C语言付费,C语言
编译器敏感度高(闭源)
适用领域CPU功耗,TDPMCU的性能表征SoC内存带宽和延迟测试CPU微架构的表征移动SoC的性能验证
运行时间~10几秒钟~10几秒钟几秒钟到几分钟>48小时~5分钟
源码获取Dhrystone-GithubCoreMark-GithubLmbench-github需购买需购买

PS:以上测试数据,主要基于手机高通sm8650平台,做实验用到的代码,大家可到github上获取参考。

Dhrystone

Dhrystone是由Reinhold P. Weicker在1984年提出来的一个基准测试程序,其主要目的是测试处理器的整数运算和逻辑运算的性能。Dhrystone首先用Ada语言发布,后来Rick Richardson为Unix开发了用C语言编写的Version 1.1,这个版本也成功的推动了Dhrystone的广泛应用。 Dhrystone标准的测试方法很简单,就是单位时间内跑了多少次Dhrystone程序,其指标单位为DMIPS/MHz。MIPS是Million Instructions Per Second的缩写,每秒处理的百万级的机器语言指令数。DMIPS中的D是Dhrystone的缩写,它表示了在Dhrystone标准的测试方法下的MIPS。 关于DMIPS有一个不得不注意的点,因为历史原因我们把在VAX-11/780机器上的测试结果1757 Dhrystones/s定义为1 DMIPS,因此在其他平台测试到的每秒Dhrystones数应除以1757,才是真正的DMIPS数值,故DMIPS其实表示的是一个相对值。(摘自参考文章1)

某种程度上,还是可以用 dhrystone 来表征CPU整形运算、逻辑运算等能力,但是浮点运算单元都不会被驱动进行工作,无法代表现在应用程序的负载行为,不会用来衡量CPU性能。但是,它有个特点,程序本身很小,以高通sm8650(8Gen3)为例,8个CPU核的 L1I$/D$ 大小均为64KB,L1$完全可以覆盖整个程序和数据,在运行时基本上数据和指令都不会跑出 L1$。我们用Android下的perf工具 simpleperf ,去实际看下笔者编出来的程序的各级cache的pmu event(基于dhrystone2.2)。

# 使用simpleperf命令
$ simpleperf stat -e raw-l1i-cache,raw-l1i-cache-lmiss,raw-l1d-cache,raw-l1d-cache-lmiss-rd,raw-l1d-cache-miss,raw-l2d-cache-lmiss-rd,raw-l2d-cache-miss,raw-l2d-cache,raw-l3d-cache,raw-l3d-cache-lmiss-rd,raw-l3d-cache-miss  ./dry 

# 忽略其他输出
# …..
Dhrystones per Second:                        25510204 

Performance counter statistics:

#           count  event_name               # count / runtime
   71,703,980,408  raw-l1i-cache            # 3.285 G/sec  
        5,221,751  raw-l1i-cache-lmiss      # 239.232 K/sec
  168,052,744,016  raw-l1d-cache            # 7.699 G/sec  
        1,853,710  raw-l1d-cache-lmiss-rd   # 84.927 K/sec 
        2,472,309  raw-l1d-cache-miss       # 113.268 K/sec
          306,598  raw-l2d-cache-lmiss-rd   # 14.047 K/sec 
          285,947  raw-l2d-cache-miss       # 13.101 K/sec 
       13,377,363  raw-l2d-cache            # 612.877 K/sec
          319,299  raw-l3d-cache            # 14.629 K/sec 
            8,418  raw-l3d-cache-lmiss-rd   # 385.666 /sec 
            5,921  raw-l3d-cache-miss       # 271.267 /sec 

我们算下cache命中率

cache misscache accesscache hit rate
Icache5,221,75171,703,980,40899.993%
Dcache2,472,309168,052,744,01699.999%

可以看出,L1I$/D$ 命中率均在99.99%以上,证实程序本身以及访存都被 L1$给包住了。因为它的这个特点,可以用来衡量CPU子系统的功耗,另外由于在跑该程序时,逻辑电路翻转的极快,短期内就会让CPU子系统的结温升上去,因此也可以用来作为SoC TDP的一个典型case。以下图高通8650为例:

8650-tdp.png

8个核同时跑dhrystone的功耗SoC功耗在11.3-13.3W之间,整体功耗在14.02-16.42W之间,测试芯片的结温在Tj在85℃。

另外,我们可以看到在不同编译选项-O和-O3下跑出来的性能相差很大,证实对编译器选项敏感。

# -O选项编译出来binary,运行结果(省略其他输出)
Dhrystones per Second:                        32154340

# -O3选项编译出来binary,运行结果(省略其他输出)
Dhrystones per Second:                        35561876 

Coremark

CoreMark是由EEMBC(Embedded Microprocessor Benchmark Consortium)的Shay Gla-On于2009年提出的一项基准测试程序,主要目标是简化操作,并提供一套测试单核处理器核心的方法。CoreMark标准的测试方法很简单,就是在某配置参数组合下单位时间内跑了多少次CoreMark程序,其指标单位为CoreMark/MHz。测试内容包含:

  • 矩阵操作(测试乘法器/加法器)
  • 链表遍历(测试内存访问)
  • 状态机(测试分支预测)
  • CRC校验(测试综合运算)

它与Dhrystone相对,更加标准化,规避编译器优化,同时也没有库函数的调用。

从8650平台实测情况看(详细请参考github仓):

# 编译选项-O跑出来的值,运行命令:taskset 80 ./coremark.exe(固定在超大核),省略其他输出
CoreMark 1.0 : 31982.090030 / GCC9.4.0 -O   -static / Heap

# 编译选项-O2跑出来的值,运行命令:taskset 80 ./coremark.exe(固定在超大核),省略其他输出
CoreMark 1.0 : 31987.205118 / GCC9.4.0 -O2   -static / Heap

可以看到 -O和-O2这两个编译选项对运行结果影响不大。

如果移植到ARM MCU上建议看下ARM的这篇文章(CoreMark Benchmarking for ARM ®Cortex ®Processors),本文不再展开。

如果想看代码详细分析,请参考CSDN上这篇文章(CPU性能测试基准(EEMBC-CoreMark)),本文不再展开。

Lmbench

lmbench,是在1994年由 Larry McVoy(SGI) 与 Carl Staelin(HP) 联合开发,旨在提供轻量级、可移植的系统级微基准测试工具。它是一款用于评估系统综合性能的跨平台开源benchmark。它能测试包括文件读写、内存操作、进程创建销毁开销及网络性能等,并提供了一个简单的测试方法。主要包括:

  • 网络性能测试:用于测量TCP/IP和UDP的延迟和带宽。
  • 文件系统性能测试:评估文件系统的读取和写入速度。
  • 内存性能测试:测试内存的带宽和延迟。
  • 进程和线程的性能测试:分析进程和线程的创建、调度及其相关性能。

我们SoC产商主要使用lmbench 内存性能测试 bandwitdh latency 这这两大类的case,它可以帮我们找到 带宽 延迟瓶颈。本文会分析测试框架、这两个case的特点、我们在系统调优的过程中如何对它进行改造,以及我们又该如何使用这两个测试case,进行软硬结合的系统调优。

lmbench框架分析

在分析程序框架之前,我们先看下 bandwidth这个case的运行说明,建立感性认识:

# 先用bw_mem --help看下具有参数
/data/local/tmp$ ./bw_mem --help  
Usage: ./bw_mem [-P <parallelism>] [-W <warmup>] [-N <repetitions>] <size> what [conflict]
what: rd wr rdwr cp fwr frd fcp bzero bcopy

# 参数说明
# -P:同时运行的线程数(默认为1)
# -W:是否预热(默认不预热)
# -N:测试重复次数(默认值为11)
# size:访存BlockSize
# what: rd wr等测试子项

# 传入相关参数,执行bw_men进行rd测试:测试CPU多线程(8个)同时发起128MB数据块大小的读访问时的带宽
./bw_mem -P 8 -W 1 -N 3 128m rd

然后,我们来看lmbench测试程序框架的主流程(流程图不太标准,但不妨碍我们说明核心流程)。

lmbench-main-flow.png

所以,benchmp为函数指针,指向命令传入的测试项的函数。我们以 bw_memrd函数为例分析。

关键流程如下:

  1. 调用 get_enough函数,粗略计算让程序运行足够长时间(1s左右)所需要的迭代次数,可以通过设置 ENOUGH环境变量(具体值应该设为多少后文再讨论);
  2. 如果parallel > 1(多线程并发,通过 -P参数传入),递归调用 benchmp,计算基线性能,同时根据这些基线参数调整运行1s左右需要的确切的迭代次数;
  3. 通过调用 fork这个API,创建子线程,入口函数为 benchmp_child,父进程的入口函数为 benchmp_parent,在此之前,需要创建一系列父子进程交互以及时序控制的signal(实际是通过linux pipe机制);
  4. 在正式进入benchmp_child之前会通过 handle_scheduler()函数处理CPU亲和性等调度策略;
  5. 调用 initialize函数指针指向的实体(该case对应的函数是 init_loop),一般是分配资源(该case对应的分配测试所需要的内存buffer);
  6. 通过传进来的命令行参数 (-W 参数)决定是否进行warmup(热身),现在很多benchmark都设置了这个控制参数,这样做的好处是让cpu cache等保持warm的状态,后续再运行测试程序得到的性能值波动较小;
  7. 循环执行最终的 benchmark函数(示例对应 的case是 rd),循环迭代次数受多个因素影响,最终将Benchmark运行的时间控制在1S左右(注意:测试框架本身的开销要刨除,后面会展开分析怎样刨除):
    • 外层循环:通过repetitions变量控制(通过 -N参数传入),为追求数据的准确性可以传入较大的数值(默认为11),如果在baremental的环境中,可以设置较小的值,以追求程序运行的速度(后文我们展开为什么在SoC产商开发初期程序速度为什么至关重要)
    • 内层循环:受 ENOUGH环境变量(如果设置了的话),由 get_enough函数决和步骤2中的因素影响
  8. 记录子进程运行结果;
  9. 在子程序退出之前,要调用 cleanup清理现场(该case对应释放分配的内存资源);
  10. 程序运行结束,将多次迭代的结果(排序过)中值作为最终运行结果。

该主程序框架几个优点,值得我们借鉴

  1. 将其抽象成标准流程,方便加入新的设计case实现特定测试(面向对象思想:将复杂留给自己,将简单留给别人);
  2. 将不同特定的系统(如baremental还是linux,背景进程的干扰完全不同)的影响考虑,让用户决定是否要对程序热身(-W参数控制),以及是否要运行最够多的次数(-N参数控制),在又快又准之间取得平衡;
  3. 刨除了自身测试框架的开销。

至此,我们可以看下,它是如何做到刨除自身测试框架的开销的。看 benchmp_interval函数的片段:

benchmp_interval(void* _state)
{
    //..省略其他部分

	if (!state->need_warmup) {
		result = stop(0,0);
		if (state->cleanup) {
			if (benchmp_sigchld_handler == SIG_DFL)
				signal(SIGCHLD, SIG_DFL);
			(*state->cleanup)(iterations, state->cookie);
		}
		save_n(state->iterations); //循环迭代次数
		result -= t_overhead() + get_n() * l_overhead(); //1:去掉程序框架的时间
		settime(result >= 0. ? (uint64)result : 0.);
	}

	switch (state->state) {
	case warmup:
		// 热身的处理,省略
		break;
	case timing_interval:
		iterations = state->iterations;
		if (state->parallel > 1 || result > 0.95 * state->enough) {
			insertsort(gettime(), get_n(), get_results());// 2:保存单次结果到已排序结果数组中
			state->i++;
			/* we completed all the experiments, return results */
			if (state->i >= state->repetitions) {
				state->state = cooldown;
			}
		}
        // 省略其他逻辑
        break;
    }
}

可以看到:

  1. 在这里去掉了测试框架的时间开销的
  2. 获取源码 标号1处保存的测试时间和迭代次数,将结果保存到有序数组中,以便测试case进一步处理准备好数据。

我们仔细阅读 t_overheadl_overhead可以看到,跟我们测试框架长的特别像,简单讲就是把测试程序的循环开销和非循环开销都去掉了:

  • l_overhead计算核心循环体开销,即多次迭代除真实运行case函数之外的开销;
  • t_overhead计算主测试框架开销。

接下来,我们看具体的测试用例集。

bandwidth带宽

基本原理

bandwidth的基本原理比较简单,就是一定时间内CPU访存数据量的大小(单位是MB/s)。先回顾下带宽测试有哪些子项:

/data/local/tmp$ ./bw_mem --help  
Usage: ./bw_mem [-P <parallelism>] [-W <warmup>] [-N <repetitions>] <size> what [conflict]
what: rd wr rdwr cp fwr frd fcp bzero bcopy

可以看到有:rdwrrdwrcpfwrfrdfcpbzerobcopy

一般会重点看3个子项,其特点如下:

  • wr:每次写4B(CPU写),间隔16B,buffer 1块
  • rd:每次读4B(CPU读),间隔16B,buffer 1块
  • cp:每次拷贝4B(CPU先读后写),间隔16B,源/目的buffer各1块
void wr(iter_t iterations, void *cookie)
{
	state_t *state = (state_t *) cookie;
	register TYPE *lastone = state->lastone;

	while (iterations-- > 0) {
	    register TYPE *p = state->buf;
	    while (p <= lastone) {
#define	DOIT(i)	p[i] = 1;
		DOIT(0) DOIT(4) DOIT(8) DOIT(12) DOIT(16) DOIT(20) DOIT(24)
		DOIT(28) DOIT(32) DOIT(36) DOIT(40) DOIT(44) DOIT(48) DOIT(52)
		DOIT(56) DOIT(60) DOIT(64) DOIT(68) DOIT(72) DOIT(76)
		DOIT(80) DOIT(84) DOIT(88) DOIT(92) DOIT(96) DOIT(100)
		DOIT(104) DOIT(108) DOIT(112) DOIT(116) DOIT(120) DOIT(124);
		p +=  128;
	    }
	}
}

wr的case代码为例。每次访存数据块大小都是4个Byte,间隔16个Byte,很有规律性。首先我们知道CPU cache和DDR之间内存交互的最小单位是cacheline,近几年的ARM移动端处理器的的CPU子系统内部的cacheline大小都是64B,这样就是在访问第一个4Byte数据块的时候,即使发生cache miss,去到DDR里访问,也会连带把后面的3个数据会跟一同填充到cache中,后续再访问就会cache hit。另外,现在处理器一般都会有硬件预取机制,这种规律的访问,硬件可以识别。具体怎么做,大家可以去看这篇Paper:《Path Confidence based Lookahead Prefetching》。从笔者跟ARM邮件交流看,ARM处理器也是支持这类预取的。

spp.png

我们接下来看下 bandwidth的带宽的计算:

  1. 这个计算出来的带宽,并不是真实的带宽值,而是使用的中值代表多线程的带宽。具体来讲
    • 当运行总次数是奇数时,会直接将中位数值当作整个程序耗时;
    • 当运行总次数是偶数时,将中位数附近两个值求平均值当作整个程序耗时;
    • 更细节的信息请参看 save_median函数
  2. 所以最后带宽计算公式为:带宽BW = BlockSize / (Total_time/(n*Parallel))
    • BlockSize :此次测试的数据块大小
    • Total_timewr测试总耗时(去掉了程序框架本身的开销)
    • n: 特定时间内(通过前文分析,我们知道在1S左右)完成的 wr测试的次数
    • Parallel:多线程(多核)并发的个数

大家看到这里,可能会有一个疑问:计算多核带宽的时候,使用中值,适用于SMP多核系统,但对于ARM这类的 big.little,显然是是有一定局限性的,最起码没反映真实的DDR带宽水平。笔者认为,可能的原因是:在lmbench程序在开发出来的时候还没有类似ARM big.little大小核这类异构CPU出现。所以,要拿到真实的ARM移动SoC的带宽测试值,需要对其进行改造。改造细节在后文优化章节再探讨。

实测分析

我们来看下高通SM8650和SM8550的超大核带宽对比,如下图所示(X3/X4是ARM超大核CPU):

lmbench-bw-comparision.jpg

预期SM8650(X4)应该会比SM8650(X3)差不多,从上图看总体趋势符合我们预期。

Latency延迟

基本原理

latency基本原理也比较简单,就是CPU不同数据块大小的CPU访存时长,单位是ns(纳秒)。

Usage [-P <parallelism>] [-W <warmup>] [-N <repetitions>] [-t] len [stride...]
# P:同时运行的线程数(默认为1)
# W:是否预热(默认不预热)
# N:测试重复次数(默认值为11)
# len:截止测试的最大数据块大小(默认值是1MB),测试起始值是512字节
# stride:步幅(默认是512字节)
# t:访存模式(默认是顺序模式;传入-t则是随机模式)

lantecy测试程序的特点如下:

  • 它从512 字节开始,逐步增加测试步长(最后一个参数指定),直到达到指定的内存大小(-N参数指定);
  • -P-W-N参数跟属于框架内容,已分析不再展开;
  • 顺序模式:易于被Hardware Prefetch Engine预测;
  • 随机模式:它通过特殊的访存方式,可以构造出 cache line misstlb miss,CPU预取硬件单元不易预测。

下面,我们随机模式和顺序模式的特点及区别。

顺序模式

顺序模式,关键函数:thrash_initialize

void stride_initialize(iter_t iterations, void* cookie)
{
	// 省略非核心代码
	base_initialize(iterations, cookie);
	addr = state->base;

	for (i = stride; i < range; i += stride) {
		*(char **)&addr[i - stride] = (char*)&addr[i];
	}
	*(char **)&addr[i - stride] = (char*)&addr[0];
	state->p[0] = addr;
	mem_reset();
}

核心代码是7到10行。以stride=64Byte、range=512为例,也就是每隔64B去发起一次读数据,一共发起range/stride=512/64=8个读操作,且这8个数据被连成 循环链表的方式(实际上range会以1MB为单位,这里只是方便说明问题,如下图所示),特别具有规律性。到这里就能明白为什么容易被硬件预取。

lmbench-stride-mode.png

随机模式

随机模式,关键函数:thrash_initialize

void thrash_initialize(iter_t iterations, void* cookie)
{
	// 省略其他非核心代码
	struct mem_state* state = (struct mem_state*)cookie;

	base_initialize(iterations, cookie);
	addr = state->base;

	if (state->len % state->pagesize) {
		state->nwords = state->len / state->line;
		state->words = words_initialize(state->nwords, state->line);
		for (i = 0; i < state->nwords - 1; ++i) {
			*(char **)&addr[state->words[i]] = (char*)&addr[state->words[i+1]];
		}
		*(char **)&addr[state->words[i]] = addr;
		state->p[0] = addr;
	} else {
		state->nwords = state->pagesize / state->line;
		state->words = words_initialize(state->nwords, state->line);

		for (i = 0; i < state->npages - 1; ++i) {
			cpage = state->pages[i];
			npage = state->pages[i + 1];
			for (j = 0; j < state->nwords; ++j) {
				cur = cpage + state->words[(i + j) % state->nwords];
				next = npage + state->words[(i + j + 1) % state->nwords];
				*(char **)&addr[cur] = (char*)&addr[next];
			}
		}
		cpage = state->pages[i];
		npage = state->pages[0];
		for (j = 0; j < state->nwords; ++j) {
			cur = cpage + state->words[(i + j) % state->nwords];
			next = npage + state->words[(j + 1) % state->nwords];
			*(char **)&addr[cur] = (char*)&addr[next];
		}
		state->p[0] = (char*)&addr[state->pages[0]];
	}
	mem_reset();
}

代码总结下来就是:跟顺序模式类似,这里仍然以8个数据为例,同样还是一个 循环链表的方式,如下图所示:

lmbench-thrash-mode.png

不同点在于:

  1. 它不再是顺序访问,访问顺序呈现随机的特点(依次访问地址单元是:4->2->6->1->5->3->7->0);
  2. 具体来讲,这种随机,体现两个不仅跨cacheline(构造 cache miss)还跨page(构造 TLB miss

大家可以通过加打印的方式,将这个链表的地址打印出来验证,这里不再展开。

实测分析

lmbench-latency-comparision.jpg

我们可以看到,SM8650(X4)和SM8550(X3)从CPU到DDR的访问latency在120ns左右(使用随机模式)。

SM8650 X4超大核配置如下:

  • L1D$: 64KB
  • L2$: 2MB
  • L3$: 12MB
  • SLC: 6MB

理论上,在64KB size、2MB、14MB、20MB附近会有latency值的跳变出现,但是实测没有出现,原因待调查。

SoC平台访存系统性能优化探讨

SoC产商,在没有真实芯片的情况,要借助EMU调试。所以,会从加快EMU运行、排除软件影响和访存系统调优等几个角度跟大家分享一些心得。

加速运行速度

用真实的芯片运行的速度很快,但在SoC开发的早期,只有EMU平台(FPGA平台更适合做功能验证,不适合做性能模拟),EMU的特点是时序和供电都非常接近真实的芯片,但它的缺点就是太慢(至少比真实芯片慢1000倍以上),所以,我们在做EMU平台跑lmbench相关测试的时候,需要尽快跑完,拿到测试结果进行分析。

首先,我们要分析程序主要耗时在哪里(各个阶段加时间戳打印即可),然后有的放矢的优化。因为该程序的运行逻辑在各个平台并不会有大的差别,所以在X86 linux服务器做试验,快速找到瓶颈:

lmbench-time-consume-on-x86.jpg

可以看到90%以上的耗时都在 get_enough函数上面(PS:Linux perf火焰图也有类似功能,用这个理论上更简单快速)。我们在高通SM8550(ARM CPU)上也复现该结论。所以,首先要想办法优化 get_enough函数的时间。

针对运行EMU慢的的优化总结如下:

  1. 经过多次测试发现第一次计算enough后,后续就不会改变,所以可以使用lmbench编译自带的 enough程序,得到相应值后,将值设置到环境变量 ENOUGH中,即可跳过enough的计算。
  2. 减少repetitions的数值,根据测试数据看,在线程数一样的情况下,1次和11次测量的带宽差别不大,但耗时基本线性增加(EMU环境下要么baremental,要么是背景进程比较少的Linux环境),我们可以通过 -N参数将其设置为1;
  3. latency的用例起始大小可改为参数可配置,现在是默认512Byte(我们L1$一般都是32KB起了)。可进一步减少 latency测试项的运行时间(直接修改lat_mem_rd.c 的69行处即可 LOWER宏替换成从命令函参数传进来的变量即可,这里不再展开)。

其他软件层面的优化

因为手机SoC平台的ARM big.little大小核的特点,导致lmbench在不同核上运行差异较大。从之前分析我们知道,lmbench的带宽其实是拿排序中间的中值作为整个带宽的代表,在多核多线程运行我们带宽case的情况下,导致这个带宽值跟真实值偏差较大。所以我们要做的是:

  1. 排除调度的影响(通过在adb shell执行 export LMBENCH_SCHED=UNIQUE,导出环境变量即可;因为程序在不同核乱窜的行为,会导致每次测试数据波动很大)
  2. 改造lmbench,改用每个核的真实带宽求和去代替中值的计算逻辑
  3. 改造lmbench,加入systrace log,直接使用systrace加快分析速度。

对于第2点:计算真实带宽的改造。请参考github仓库代码,这里给出大致思路,最终统计还需要进一步改造代码或者对打印输出数据进行二次处理亦可:

void benchmp_parent(...)
{
    // 省略其他代码
  
    /* send 'exit' signals */
	write(exit_signal, results, parallel * sizeof(char));

#ifdef PRINT_ACCURATE_RESULT //打印出中间结果即可
	for (j = 0; j < merged_results->N; ++j) {
		if (merged_results->v[j].n > 1)
			fprintf(stderr, "merged_results[%d] = [%d %d]\n", j, merged_results->v[j].n, merged_results->v[j].u);
	}
#endif

	/* Compute median time; iterations is constant! */
	set_results(merged_results);
  
    // 省略其他代码
}

思路:针对 merged_results数组按照cluster的具体配置,在每个之间取平均数。最后将各个核的平均值再求和即可得到真实的访存带宽(一般SoC产商都会有类似bus monitor的带宽监测单元,可以监测DDR带宽,最终的正确性还得用这个monitor去验证下才够严谨)。

对于第3点:将应用log打印到 systrace,主要借助 ftracetrace_marker机制。核心代码如下所示:

#define WRITE_OFFSET   64
void trace_begin(const char* message) {
  // 省略非核心代码
  int length = strlen(message) + WRITE_OFFSET;
  char buf[length];
  sprintf(buf, "B|%d|%s lmbench", getpid(), message);

  write(trace_marker_fd, buf, length); //核心就在这句!!!
}

我们可以看到实际就是往 /sys/kernel/tracing/trace_marker,写入特定格式的字符即可。在设备侧调试要点如下所示:

# open tracing_mark_write switch
echo 'trace_printk' > /sys/kernel/tracing/trace_options
echo 'markers' > /sys/kernel/tracing/trace_options
echo 1 > /sys/kernel/tracing/tracing_on

# 使用ftrace验证是否有输出(后面执行bw_mem查看带宽,若有输出bw_mem,表明systrace log加入成功)
cat /sys/kernel/tracing/trace_pipe | grep tracing_mark_write

# 在其他adb shell窗口执行,
./bw_mem -P 8  1m rd

访存系统性能调优

有了上述基础,我们再着手集中进行硬件调优,才会比较高效,否则搅和在一起很难分清楚问题在哪里,而且效率又极其低下。该阶段,主要debug手段是通过EMU仿真波形信号的分析。比如,发现了SLC(ci700)的问题(后续有机会我们单独分析下SLC,这里只做简要说明):

  • latency较大而且各个XP之间可能不均衡(XP是 cross point缩写,该IP是从CMN-700改造过来的,Mesh结构天生不适合移动端设备,据说ARM的下一代ni-tower会改进);
  • 每个cross point提供的带宽能力不足(POCQ,可以简单理解成outstanding能力不够)

另外,我们还发现CHI的 prefethTgt竞品如高通sm8650,支持的很好,而我们平台并没有支持,这块对CPU性能影响较大。DDR的配置包括、总校交织方式都需要经过反复参数调优,才能是访存系统达到预期性能。这里涉及项目敏感信息,不便。

本文完,cpu benchmark的下篇会介绍SPEC CPU 2006/2017和Geekbench v5/v6。敬请期待~ PS:本文同步在微信公众号和知乎同步发布

公众号二维码.jpg

参考