原文在这里
简介
缓存利用率低下对于性能负面影响很大,提高缓存利用率势在必行。不幸的是很难发现哪里缓存利用率低下,而且需要大量的开发时间。在本指南里,我将演示用Streamline指引缓存优化,发现利用率低下的地方。本文基于Juno,但是我使用的这些counter应该在所有Cortex-A系列处理器上都有,所以这也很容易复现。即使你没有这样的平台去测试,我在文中使用的方法也可以启示你用Streamline去指导缓存优化。这份指南默认你掌握了基本的Streamline知识,介绍信息和上手指南可以在网上的DS-5文档里和其他教材里找到。
搭建Streamline
先在Juno上装上Gator,本文不会介绍如何安装,参见<DS-5 installation dir>/arm/gator/里的readme可以找到详细信息。安装好后,启动Gator,我成功使用了用户态和内核态2个版本的Gator。用户态的Gator对大部分用例来说足够,内核态的Gator只是偶尔使用,我会在后面解释这一点。
编译附件中的cache-test,它很简单,本地编译,交叉编译都可以
配置DS-5
打开DS-5中的Stream Data视图,使用 Capture & analysis options () 配置Streamline连接运行在目标板上的Gator。默认配置应该足够满足需求,然而你可以添加带symbol的可执行文件到Program Image里面以得到函数级别的分析信息
配置 Counter Configuration()采集事件:
*Cortex-A57
Cache: Data access
Cache: Data refill
Cache: L2 data access
Cache: L2 data refill
在我们的例子中,我们也采集“Cache: Data TLB refill”事件,这会提供分析cache性能辅助指标,还有 “Clock: Cycle”和“Instruction: Executed”,这可以提供程序执行进度。我们同时也搜集Juno开发板上提供的功耗计数器。
目标计数器的更多信息
上面列出的计数器是针对我们特殊的Juno开发平台。它使用2个A57和4个A57的大小核配置;我们将在其中一个A57上运行我们的程序。
ARM性能监控扩展计数器只是一个可选的非侵入式的调试组件,在大多数Cortex-A系列处理器上都有。Streamline读取性能监控单元(PMU)提供的信息来产生分析信息。Streamline里面观测到处理器的每个计数器对应着一个PMU事件。并不是每个PMU架构里面定义的事件都会在CPU里面实现,但是一组核心的事件必须被实现。其中包括前面提到的“Cache: Data access”和“Cache: Data refill”(in PMUv2 and PMUv3).所以这二个事件应该在所有实现PMU架构的Cortex-A系列处理器上都有。更多信息可以参考ARM架构文档。
“Cache: L2 data access”和“Cache: L2 data refill”计数器在包含集成的L2缓存控制器的SoC中也很常见,但是一些SoC有独立的L2缓存控制器,比如Corelink2级缓存控制器L2C-310.在这种情况下,计数器的支持取决于控制器是否提供以及Streamline是否支持。对应L2C-310,它包含这二个计数器,而且Streamline也支持,但只能在内核空间才能读到这些计数器的信息。即使L2缓存中的这些计数器信息读不到,最终L1缓存计数器也会给出一个清晰的看到当前的状况,仍然可以用文中下面的方法进行缓存优化,只是观测数据在缓存系统中移动的全貌比较难观测一些。
大部分核同时提供其他的PMU事件(各个核可能不一样)来观察缓存利用率,这些计数器可以提供更多信息。
选择计数器
“Cache: Data access”会记下所有对L1数据缓存的读写,无论是否命中
“Cache: Data refill”会记下数据的读写导致当前L1缓存需要从其他L1或者拿数据更新,也就是存取L1数据导致缺失。和上面一样这不包括缓存维护指令,也不包含因为上次的缺失导致的数据更新
“Cache: L2 data access”和“Cache: L2 data refill”计数器测量L2缓存的相应信息
关于这些事件更详细的信息可以在ARM架构文档里面的性能监控扩展章节找到
抓取数据
配置好目标板后,按下Start Capture按钮(),一旦开始,在目标板上运行cache-test(./cache-test).依赖于你的目标板性能,这个程序需要跑几秒才能结束,并会输出几条信息才返回命令行。返回命令行后,按下Stop capture and analyze按钮()。短暂的停顿后,分析数据就显示出来了。
重新格式化获取的数据
现在你可以得到一幅和下图差不多的图表
在上图的进程列表点击“[cache-test #<proc-id>]”滤除cache-test。如果有多个感兴趣的进程,可以按住Ctrl进行多选。取决于抓取时长和程序运行时间,这里可能会占据可观的磁盘。
在上面的图表中,选择时间栏的左边下拉栏的设置分辨率,这里选择100ms进行缩放
目前图标上显示的缓存内存还很难理解,应他们的范围不一致。使用下面的方法将“Cache: Data access”和“Cache: L2 Data access”分开
1.点击进程列表中的Charts Snippet菜单()
2.选择Add Blank Chart,输入“Cache Accesses”作为新的图表名,并将该图标拖拽到“Cache”上面
3.在Cache图表上,打开Configuration Panel ()
4.将“Cache”改为“Cache Refills”
5.使用handle(),将 “Data access”和“L2 data access”图表拖动到新建的“Cache Accesses”图表
6.删除“Cache Accesses”图表中空白的“Required”图表()
7.将标识方式由堆叠式改为覆盖式(使用左上方的Configuration Panel),这样然不同图表直接的关系更直观,在覆盖模式下,图表是从上往下一层层画,而且是不同颜色透明的,所以不会覆盖任何数据。
8.可以将图表改成合适名称-比如“Data access”叫做“L1 data access”
9.也可以将图表的颜色稍做修改,对比更明显
10.点击XXX,关闭“e Configuration Panel”()
将这2个图分离开后,你得到的图表应该和下图类似:
下面我们要创建一些定制的图表来显示缓存的性能
1.点击进程列表上方的Charts Snippet菜单()
2.选择Add Blank Chart,输入“Cache Refill Ratios”作为图表名,将它拖拽到 “Cache Refill”图表下
3.将新序列名改为“L1 data ratio”,在 Expression中输入“$CacheDataRefill / $CacheDataAccess”,因为输出是比例,所以选上Percentage框
4.在“Cache Refill Ratios”()添加新序列,重复上面的步骤添加L2缓存,设置 Expression为“$CacheL2DataRefill / $CacheL2DataAccess”.如果使用一个独立的L2缓存控制器,这个公式会不一样。按住Ctrl+Space
5.将绘制方式由Stacked改为Overlay
6.可以将图表的的颜色更改以增加对比
这只是一个简单的例子,但是你可以在新图表上组合生成任意表达式,Streamline User Guide (Section 6.21)有更多描述
这将生成一个类似于下面的图表:
在我们的例子中时钟频率(133M)有误,因为它吧6个核频率做了平均,而实际上有5个核是关闭的
理解抓取的数据
组织好抓取的数据后我们来分析分析发生了什么
这段代码主要分为三段,开始的200ms,Cache活动率较低,后面的100ms
.大量的一级数据缓存访问(50.2M)
.几乎同样的一级和二级数据缓存refill(1.5M)
.少量的一级数据TLB refill(26k)
.一级数据缓存refill率很低(3.1%),二级数据缓存refill率相对较高(33.2%)
这意味着处理了很多数据,而且缓存利用率很高。相对较高的二级数据缓存refill有点问题,但一级缓存refill率低意味这二级缓存访问量不高,这个也可以通过对比较低的二级缓存访问(4.7M)和较高的一级缓存访问(50.2M)证实。当有新数据需要从内存获取时,二级缓存一定会执行一些refill。
接下来的2200ms:
.大量的一级数据缓存访问(81.5M),但一级数据访问平率明显减少(37M/s,上一阶段是502M/s)
.一级数据缓存refill激增(26.9M)
.二级数据缓存refill相似(2.1M)
.一级数据缓存TLB refill激增(24.9M)
.一级数据缓存refill率大增(33%),二级数据缓存refill率(2.03%)降低
这意味着这一阶段和前一阶段处理的数据量差不多(二级缓存refill次数差不多意味着从内存中获取数据的量类似),但是缓存利用率较低(一级缓存refill率较高)
用streamline分析应用的时候要注意这种模式,这常常意味着缓存利用率可以提高。因为一级数据缓存refill率高而二级数据缓存refill率低意味着一级缓存存在抖动。如果二级数据缓存refill率也很高,意味着程序造成二级缓存也在颠簸,但这也可能是程序消耗的数据源格式比较特殊,对于特殊情况优化空间不大。但当同样的数据被操作多次一般都有很大的优化空间。
在我们的例子中cache-test对一个大型的二维矩阵的行进行二次求和,一次以行顺序:
for (y = 0; y < iterations; y++)
for (x = 0; x < iterations; x++)
sum_1d[y] += src_2d[(y * iterations) + x];
第二次以列顺序
这意味着缓存没有利用数组的空间相关性,这会导致一级数据缓存TLB refill的次数从一个很小的值跳变到26.9M。TLB(传输后备缓冲器)是一个小的页表缓存:Cortex-A57的一级数据TLB是一个32条的全关联缓存。TLB里面的大量缺失意味着频繁的跨页面的非连续内存访问,正如我们的例子中观测到的。
cache-test例子操作一个5000x5000的int32矩阵-95.4MB数据.Cortex-A57使用64byte缓存行,最少可以缓存1.56M数据,这也解释了一级二级数据缓存在第一阶段的refill量(1.57M)一致,数据是有序访问,也解释为什么即使在最优情况下也就这么高。
修复问题
在这个简单的例子中,我们可以通过改变内外循环的顺序提高缓存利用率,这样没有任何代价也取得了性能的显著提高(在我们的例子中,提高了22倍)
在现实的例子中,可能很难定位哪里缓存利用率不高,Streamline的代码视窗可以帮助定位这个问题.为了利用这个功能,需要加载应用的Symbol,要么向前面描述的一样运行前加载程序或者抓取数据后点击Streamline Data视图,选择Analyze...添加程序。如果程序包含调试符号表,在Code栏就可以看到源码级别的调试信息,否则只能看函数栏看到函数信息。
函数级别的信息给我们提供很好的定位信息。如果函数符号表也有的话,通过点击函数栏目中的函数,在代码视窗可以看到类似下面的图表
代码左边的标签表示这行代码在取样的过程中执行了多少次,比例表示此行相对于函数中其他行取样的比例。使用TimeLine的取样HUD(),我们可以发现第二阶段的采样多次落在“yx_loop”函数中,点击Sample HUD或者函数栏中的这个函数,我们可以看到总共有1584个取样落在内循环,这意味着我们需要看看内循环。在我们的例子中,内循环函数很简单,如果它复杂一些,streamline会给出一个更好的提示哪里花费了大量的时间。
总结
我随文附上了简单的cache-test例子,目前他还在添加到DS-5内建例子的过程中,所以将来版本的DS-5会包含它。当DS-5发行版包含它的时候,我会在后面更新这篇blog。
http://community.arm.com/servlet/JiveServlet/download/38-18806/cache-test-source.zip