-
Intel CPU 上使用 pmu-tools 进行 TopDown 分析
2021-01-27 20:13:28title: Intel CPU 上使用 pmu-tools 进行 TopDown 分析 date: 2021-01-24 18:40 author: gatieme tags: - debug - linux - todown categories: - debug thumbnail: blogexcerpt: 这篇文章旨在帮助希望更好地分析其...
title: Intel CPU 上使用 pmu-tools 进行 TopDown 分析
date: 2021-01-24 18:40
author: gatieme
tags:
- debug
- linux
- todown
categories:
- debugthumbnail:
blogexcerpt: 这篇文章旨在帮助希望更好地分析其应用程序中性能瓶颈的人们. 有许多现有的方法可以进行性能分析, 但其中没有很多方法既健壮又正式. 而 TOPDOWN 则为大家进行软硬协同分析提供了无限可能. 本文通过 pmu-tools 入手帮助大家进行 TOPDOWN 分析.
CSDN GitHub OSKernelLAB 紫夜阑珊-青伶巷草 LDD-LinuxDeviceDrivers
Intel CPU 上使用 pmu-tools 进行 TopDown 分析
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可, 转载请注明出处, 谢谢合作.
因本人技术水平和知识面有限, 内容如有纰漏或者需要修正的地方, 欢迎大家指正, 鄙人在此谢谢啦
1 Topdown & PMU-TOOLS 简介
1.1 TopDown 简介
程序的执行效率是完全依赖与 CPU 执行的, 软件的性能优化, 除了通用的一些优化外, 更重要的是软硬协同优化, 充分发挥 CPU 的硬件特性. 因此我们软件上的一些性能问题, 可以在 CPU 执行流程中的某一环直观的显现出来. 但是由于 CPU 的复杂性, 普通用户和软件开发人员很难也很少有经历去弄懂 CPU 架构, 更不用谈从 CPU 架构上理解软件上那一块出现的问题.
如果有一套软硬协同的分析方法, 可以帮助用户快速了解/分析/定界/定位 当前应用在 CPU 上的性能瓶颈, 用户就可以针对当前 CPU 有针对性的修改自己的程序, 以充分利用当前的硬件资源.
因此 Intel 提出了一套软硬协同进行性能分析的更正式的方法. 它称为自上而下的微体系结构分析方法(TMAM) (<英特尔®64和IA-32架构优化参考手册> 附录B.1). 在这种方法论中, 我们尝试从高层组件(如前端, 后端, 退休, 分支预测器)开始检测导致执行停滞的原因, 并缩小性能低效的根源.
进行 TOPDOWN 分析的过程通过是一个不断迭代优化的过程, 他的通常流程如下
-
确定性能问题的类型;
1.1. 我们可以先从高层次的划分中, 先粗粒度的当前性能问题主要问题是在哪个阶段(类型).
1.2. 接着从低层次的划分中, 继续明确具体是哪块出的问题. -
使用精确事件 PEBS(X86)/SPE(ARM64) 在代码中找到确切的位置;
2.1. 如果条件运行, 精确的抓取对应的 PMU 事件, 明确代码中出现问题的原因和位置. -
解决性能问题后, 重新开始 1.
3.1. 修改代码, 解决性能问题, 然后重新进行 TOPDOWN 分析, 验证修改是否 OK, 看是否还有其他瓶颈或者是否引入其他问题.
1.2 TopDown 资料汇总
Top-down Microarchitecture Analysis Method(TMAM)资料
之前介绍过TMAM的具体内容, 在这里对网络上相关的信息和资料做一个汇总:-
国外资料
Tuning Applications Using a Top-down Microarchitecture Analysis Method
Top-down Microarchitecture Analysis through Linux perf and toplev tools
A Top-Down method for performance analysis and counters architecture
Performance_Analysis_in_a_Nutshell
Top Down Analysis Never lost with Xeon® perf. counters
How TMA* Addresses Challenges in Modern Servers and Enhancements Coming in IceLake
-
国内资料
几句话说清楚15:Top-Down性能分析方法资料及Toplev使用
1.3 PMU-TOOLS 简介
pmu-tools 是 Intel 的 Adni Kleen 开发的帮助用户和开发者在 Intel X86 CPU 下进行 TOPDOWN 分析的开源工具包, 可以定位和分析 CPU Bound 代码的瓶颈, 不能识别其他(Not bound by CPU)代码的瓶颈.
2 PMU-TOOLS 安装
2.1 下载 github 仓库
git clone https://github.com/andikleen/pmu-tools
2.2 下载 PMU 表
pmu-tools 完成的具体工作就是采集 PMU 数据, 并按照微架构既定的公式计算 topdown 数据, 但是不同的微架构和 CPU 所支持的 PMU 事件是不同的, 因此 pmu-tools提供了一套完成的映射表, 标记不同 CPU 所支持的 PMU 事件映射表, 以及 topdown 如何使用这些 PMU 数据去计算, 参见 Intel 01.day perfmon 下载.
pmu-tools 的工具在第一次运行的时会通过
event_download.py
把本机环境的 PMU 映射表自动下载下来, 但是前提是你的机器能正常连接 01.day 的网络. 很抱歉我司内部的服务器都是不行的, 因此 pmu-tools 也提供了手动下载的方式.因此当我们的环境根本无法连接外部网络的时候, 我们只能通过其他机器下载实际目标环境的事件映射表下载到另一个系统上, 有点交叉编译的意思.
- 首先获取目标机器的 CPU 型号
printf "GenuineIntel-6-%X\n" $(awk '/model\s+:/ { print $3 ; exit } ' /proc/cpuinfo )
cpu的型号信息是由 vendor_id/cpu_family/model/stepping 等几个标记的.
他其实标记了当前 CPU 是哪个系列那一代的产品, 对应的就是其微架构以及版本信息.
注意我们使用了 %X 按照 16 进制来打印
注意上面的命令显示制定了 vendor_id 等信息, 因为当前服务器端的 CPU 前面基本默认是 GenuineIntel-6 等.
不过如果我们是其他机器, 最好查看下 cpufino 信息确认.
比如我这边机器的 CPU 型号为 :
processor : 7 vendor_id : GenuineIntel` cpu family : 6 model : 85 model name : Intel(R) Xeon(R) Gold 6161 CPU @ 2.20GHz stepping : 4 microcode : 0x1
对应的结果就是
GenuineIntel-6-55-4
.我们也可以直接用
-v
打出来 CPU 信息.$ python ./event_download.py -v My CPU GenuineIntel-6-55-4
- 获取 PMU 映射表
如果你可以链接网络, 那么这个命令就会把你
host
机器上的PMU
映射表下载下来. 否则你拿到了CPU
型号, 那么你可以直接显式指定CPU
型号来获取.$ python ./event_download.py GenuineIntel-6-55-4 Downloading https://download.01.org/perfmon/mapfile.csv to mapfile.csv Downloading https://download.01.org/perfmon/SKX/skylakex_core_v1.24.json to GenuineIntel-6-55-4-core.json Downloading https://download.01.org/perfmon/SKX/skylakex_matrix_v1.24.json to GenuineIntel-6-55-4-offcore.json Downloading https://download.01.org/perfmon/SKX/skylakex_fp_arith_inst_v1.24.json to GenuineIntel-6-55-4-fp_arith_inst.json Downloading https://download.01.org/perfmon/SKX/skylakex_uncore_v1.24.json to GenuineIntel-6-55-4-uncore.json Downloading https://download.01.org/perfmon/SKX/skylakex_uncore_v1.24_experimental.json to GenuineIntel-6-55-4-uncoreexperimental.json my event list /home/chengjian/.cache/pmu-events/GenuineIntel-6-55-4-core.json
最终 PMU 的映射表文件, 将被保存在
~/.cache/pmu-events
. 将此目录打包后, 放到目标机器上解压即可.当前我们也可以使用
event_download.py -a
下载所有可用的 PMU 事件映射表. 然后通过如下环境变量, 将 PMU 映射手动指向正确的文件.export EVENTMAP=~/.cache/pmu-events/GenuineIntel-6-55-4-core.json export OFFCORE=~/.cache/pmu-events/GenuineIntel-6-55-4-offcore.json export UNCORE=~/.cache/pmu-events/GenuineIntel-6-55-4-uncore.json
最后两个
offcore/uncore
的设置是可选的. 或者可以将名称设置为 CPU 标识符(GenuineIntel-FAM-MODEL), 这将覆盖当前 CPU.另一种方法是显式设置目标机器的 PMU 映射文件存储路径( 注意是在相对于它的 pmu-events 目录中).
export XDG_CACHE_DIR=/opt/cache
则将从
/opt/cache/pmu-events
中查找对应的映射文件.3 PMU-TOOLS 使用
3.1 准备工作
特别需要注意的是, pmu-tools 的大多数工作在使用期间都建议我们禁用 NMI watchdog, 并以 root 身份运行.
因为 X86_64 的 NMI_WATCHDOG 使用了一些 PMU 的寄存器, 同时我们在抓取的时候可能耗时过长, 存在出发 NMI_WATCHDOG 的影响.
Consider disabling nmi watchdog to minimize multiplexing (echo 0 > /proc/sys/kernel/nmi_watchdog as root)
使用如下命令关闭
sysctl -p 'kernel.nmi_watchdog=0' OR echo 0 > /proc/sys/kernel/nmi_watchdog
3.1 toplev
toplev
是一个基于perf
和TMAM
方法的应用性能分析工具. 从之前的介绍文章中可以了解到TMAM
本质上是对CPU Performance Counter
的整理和加工. 取得Performance Counter
的读数需要perf
来协助, 对读数的计算进而明确是Frondend bound
还是Backend bound
等等.在最终计算之前, 你大概需要做三件事:
- 明确 CPU 型号, 因为不同的 CPU, 对应的 PMU 也不一样
- 读取 TMAM 需要的 perf event 读数
- 按 TMAM 规定的算法计算, 具体算法在这个 Excel 表格里
这三步可以自动化地由程序来做. 本质上
toplev
就是在做这件事.toplev的Github地址: https://github.com/andikleen/pmu-tools
另外补充一下, TMAM作为一种Top-down方法, 它一定是分级的. 通过上一级的结果下钻, 最终定位性能瓶颈. 那么toplev在执行的时候, 也一定是包含这个“等级”概念的.
下面是
toplev
使用方法的资料:基本上都是由 toplev 的开发者自己写的, 可以作为一个 Quick Start Guide.
首先可以进行 L1 级别的检查
$ python toplev.py --core S0-C0 -l1 -v --no-desc taskset -c 0 ./a.out ... S0-C0 FE Frontend_Bound: % Slots 19.2 < S0-C0 BAD Bad_Speculation: % Slots 4.6 < S0-C0 BE Backend_Bound: % Slots 4.1 < S0-C0 RET Retiring: % Slots 72.1 < S0-C0-T0 MUX: % 100.0 S0-C0-T1 MUX: % 100.0
接着进行
level2/level3
级别的检查, 最大支持到 level6.$ python toplev.py --core S0-C0 -l2 -v --no-desc taskset -c 0 ./a.out $ python toplev.py --core S0-C0 -l3 -v --no-desc taskset -c 0 ./a.out
toplev 工具是基于 perf 等工具进行采样的, 因此多数这些工作支持的行为, toplev 也是支持的.
-
默认情况下, toplev 同时测量内核和用户代码. 如果只对用户代码感兴趣, 则可以使用
--user
选项. 这往往会减少测量噪声, 因为中断被过滤掉了. 还有一个--kernel
选项用来测量内核代码. -
在具有多个阶段的复杂工作负载上, 测量间隔也是有用的. 这可以用
-I xxxi
选项指定,xxx
是间隔的毫秒数. perf 要求时间间隔至少需要 100ms. toplev 将输出每个间隔的测量值. 这往往会产生大量的数据, 所以绘制输出很有必要.
更多详细信息可以参照 topdev 的 help 帮助文档.
4 参考资料
Top-Down performance analysis methodology
-
本作品/博文 ( AderStep-紫夜阑珊-青伶巷草 Copyright ©2013-2017 ), 由 成坚(gatieme) 创作.
-
采用
知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可. 欢迎转载、使用、重新发布, 但务必保留文章署名成坚gatieme ( 包含链接: http://blog.csdn.net/gatieme ), 不得用于商业目的.
-
基于本文修改后的作品务必以相同的许可发布. 如有任何疑问, 请与我联系.
-
-
python分析web日志文件
2020-01-08 22:58:23# -*- coding: utf-8 -*- import time import os import json def remove_logs(cachedir... for root, dirs, files in os.walk(cachedir, topdown=False): for name in files: os.remove(os.path.join(root, n...# -*- coding: utf-8 -*- import time import os import json def remove_logs(cachedir): for root, dirs, files in os.walk(cachedir, topdown=False): for name in files: os.remove(os.path.join(root, name)) for name in dirs: os.rmdir(os.path.join(root, name)) def runlog(filelog,filename=''): print('[running]','初始化数据') spiderr = ['Googlebot','bingbot','Baiduspider','360Spider','Sogou','DotBot','Yahoo','Trident','YandexBot','SiteExplorer','MJ12bot','AhrefsBot','ExtLinksBot','SEOkicks-Robot','SiteExplorer','YisouSpider','python','SemrushBot'] dataname = [] dataval = [] timeline = '' timesize = 0 sizelevel = [0 for i in range(11)] sizeall = 0 print('[running]','开始遍历数据',filelog) with open(filelog, 'r') as files: for line in files: # print(line) r = line.strip().split('\t') if len(r) != 8:continue # 处理各网站蜘蛛抓取情况 r[2] = r[2].lower() for spider in spiderr: if spider.lower() in r[7].lower(): sp = spider.lower() break else: sp ='none' # r.insert(7,sp) m = [r[2],sp] if m not in dataname: dataname.append(m) dataval.append(1) else: key = dataname.index(m) dataval[key]+=1 # 处理每秒压力情况 # print(r[5]) size = int(r[5]) if timeline != r[1]: # print(timesize) mb = int(timesize*8/1024/1024) # print(mb,timesize/1024/1024,timesize) if mb>10:mb=10 sizelevel[mb] += 1 timesize = size timeline = r[1] else: timesize += size sizeall += size print('[running]','数据抓取完成') # 整理抓取数据 webr = [] for i in range(len(dataname)): webr.append({ 'domain':dataname[i][0], 'spider':dataname[i][1], 'value':dataval[i], }) # 汇总数据 data = { 'web-spider':webr, 'size-level':sizelevel, 'size-count':sizeall } print(data) if filename: print('[running]','执行保存文件',filename) # 保存新数据 with open(filename, 'w', encoding='utf-8') as filejson: filejson.write(json.dumps(data)) print('[running]','执行删除文件',filelog) remove_logs(filelog) if __name__ == '__main__': date = time.strftime("%Y%m%d",time.localtime(int(time.time()))) filelog = 'Access.%s.log'%date filename = 'log.%s.json'%date runlog(filelog,filename) # 脚本说明: # python3.6版本 # 数组spiderr 蜘蛛名列表,不在数组内统一为none,none包括真实用户请求 # size-level 指每秒使用带宽数据,单位M [1秒内使用0~1M带宽次数,...,1秒内使用10M~maxM带宽次数] # size-count 指web日志请求总大小,单位bit 换算MB size/1024/1024 # parama filelog string 日志文件名 # parama filename string 保存文件名 # 需配合CheckLog.cut.sh使用 # YESTERDAY=$(date -d "yesterday" +%Y%m%d) # mv /home/wwwlogs/access.log /home/wwwlogs/Access.${YESTERDAY}.log # kill -USR1 $(cat /usr/local/nginx/logs/nginx.pid) # cd /home/wwwlogs/ # zip -q Access.${YESTERDAY}.log.zip Access.${YESTERDAY}.log # #rm -f /home/wwwlogs/Access.${YESTERDAY}.log # python3 CheckLog.py # 设置变量YESTERDAY # 重命名log文件 # 重启nginx进程,生成新log文件 # 切换到目录 # 打包压缩日志文件 # 删除当天日志文件 已注释,交给python完成 # 启动python执行日志处理脚本 # 需配合nginx.conf设置使用 # http 添加代码 # log_format main '$remote_addr [$time_local] $http_host $status "$request" $body_bytes_sent $http_referer $http_user_agent'; # service 添加代码 # access_log /home/wwwroot/xxxxx/access.log main; # 特别说明 # 无需再单独设置定时执行CheckLog.py,直接在CheckLog.cut.sh内执行即可 # by 薛一 # 2018-07-12
-
PHP内核分析-Zend引擎-栈结构及操作
2014-02-11 16:27:22#define ZEND_STACK_APPLY_TOPDOWN 1 //由顶部向底部对栈进行遍历操作 #define ZEND_STACK_APPLY_BOTTOMUP 2 //由底部向顶部对栈进行遍历操作 #define STACK_BLOCK_SIZE 64 看一下Zend引擎栈的数据结构定义: ...Zend引擎对栈常量的定义:
#define ZEND_STACK_APPLY_TOPDOWN 1 //由顶部向底部对栈进行遍历操作 #define ZEND_STACK_APPLY_BOTTOMUP 2 //由底部向顶部对栈进行遍历操作 #define STACK_BLOCK_SIZE 64 //每次扩容的元素个数
看一下Zend引擎栈的数据结构定义:typedef struct _zend_stack { int top; //栈顶,用数字表示(从底到顶分别为0,1,2...) int max; //栈当前最大元素个数,当栈满后会将max+STACK_BLOCK_SZE来进行扩容 void **elements; //栈元素指针数组 } zend_stack;
栈的常用操作如下://初始化栈,不包含zend_stack结构的内存分配。仅初始化top, max的值和elements的值。 ZEND_API int zend_stack_init(zend_stack *stack) { stack->top = 0; stack->max = 0; stack->elements = NULL; return SUCCESS; } //以memcpy的方式将element元素复制入栈。size表示元素的大小。 ZEND_API int zend_stack_push(zend_stack *stack, const void *element, int size) { if (stack->top >= stack->max) { /* we need to allocate more memory */ stack->elements = (void **) erealloc(stack->elements, (sizeof(void **) * (stack->max += STACK_BLOCK_SIZE))); //当栈满后会将max+STACK_BLOCK_SZE来进行扩容 if (!stack->elements) { return FAILURE; } } stack->elements[stack->top] = (void *) emalloc(size); memcpy(stack->elements[stack->top], element, size); return stack->top++; } //获取栈顶部元素的指针,并赋值给**element ZEND_API int zend_stack_top(const zend_stack *stack, void **element) { if (stack->top > 0) { *element = stack->elements[stack->top - 1]; return SUCCESS; } else { *element = NULL; return FAILURE; } } //删除栈顶。 ZEND_API int zend_stack_del_top(zend_stack *stack) { if (stack->top > 0) { efree(stack->elements[--stack->top]); } return SUCCESS; } //返回栈顶元素的指针的值 ZEND_API int zend_stack_int_top(const zend_stack *stack) { int *e; if (zend_stack_top(stack, (void **) &e) == FAILURE) { return FAILURE; /* this must be a negative number, since negative numbers can't be address numbers */ } else { return *e; } } //根据stack->top的值来判断栈是否为空.stack->top用数字来表示的好处。 ZEND_API int zend_stack_is_empty(const zend_stack *stack) { if (stack->top == 0) { return 1; } else { return 0; } } //销毁栈。其实也可以while(stack->top--){zend_stack_del_top(stack)};stack->elements = NULL; ZEND_API int zend_stack_destroy(zend_stack *stack) { int i; if (stack->elements) { for (i = 0; i < stack->top; i++) { efree(stack->elements[i]); } efree(stack->elements); stack->elements = NULL; } return SUCCESS; } //直接返回栈数据元素指针 ZEND_API void **zend_stack_base(const zend_stack *stack) { return stack->elements; } //返回栈元素个数。这就是用数字来表示栈顶而不直接用指针来表示的好处。 ZEND_API int zend_stack_count(const zend_stack *stack) { return stack->top; } //遍历栈并执行函数。注意,传入的函数返回值为int型。为0时表示函数执行成功,为1时表示函数执行失败。 ZEND_API void zend_stack_apply(zend_stack *stack, int type, int (*apply_function)(void *element)) { int i; switch (type) { //自顶向下执行 case ZEND_STACK_APPLY_TOPDOWN: for (i=stack->top-1; i>=0; i--) { if (apply_function(stack->elements[i])) { break; } } break; //自底向上执行 case ZEND_STACK_APPLY_BOTTOMUP: for (i=0; i<stack->top; i++) { if (apply_function(stack->elements[i])) { break; } } break; } } //与zend_stack_apply函数一样,都是遍历栈并执行函数。但此函数支持调用函数传递参数。 ZEND_API void zend_stack_apply_with_argument(zend_stack *stack, int type, int (*apply_function)(void *element, void *arg), void *arg) { int i; switch (type) { case ZEND_STACK_APPLY_TOPDOWN: for (i=stack->top-1; i>=0; i--) { if (apply_function(stack->elements[i], arg)) { break; } } break; case ZEND_STACK_APPLY_BOTTOMUP: for (i=0; i<stack->top; i++) { if (apply_function(stack->elements[i], arg)) { break; } } break; } }
-
python读取整个文件的方法是_教大家一个python获取目录下所有文件的方法
2020-12-18 13:58:04教大家一个python获取目录下所有文件的方法具体分析如下:os.walk()函数声明:walk(top,topdown=True,onerror=None)1. 参数top表示需要遍历的目录树的路径2. 参数topdown的默认值是"True",表示首先返回目录树下的...教大家一个python获取目录下所有文件的方法
具体分析如下:
os.walk()
函数声明:walk(top,topdown=True,οnerrοr=None)
1. 参数top表示需要遍历的目录树的路径
2. 参数topdown的默认值是"True",表示首先返回目录树下的文件,然后在遍历目录树的子目录.Topdown的值为"False"时,则表示先遍历目录树的子目录,返回子目录下的文件,最后返回根目录下的文件
3. 参数onerror的默认值是"None",表示忽略文件遍历时产生的错误.如果不为空,则提供一个自定义函数提示错误信息后继续遍历或抛出异常中止遍历
4. 该函数返回一个元组,该元组有3个元素,这3个元素分别表示每次遍历的路径名,目录列表和文件列表
def getListFiles(path):
assert os.path.isdir(path), '%s not exist.' % path
ret = []
for root, dirs, files in os.walk(path):
print '%s, %s, %s' % (root, dirs, files)
for filespath in files:
ret.append(os.path.join(root,filespath))
return ret
print len(getListFiles('.'))
-
python获取目录下所有文件的方法
2020-12-25 10:11:53具体分析如下: os.walk() 函数声明:walk(top,topdown=True,onerror=None) 1. 参数top表示需要遍历的目录树的路径 2. 参数topdown的默认值是”True”,表示首先返回目录树下的文件,然后在遍历目录树的子目录.... -
python删除txt指定列_python删除指定类型(或非指定)的文件实例详解
2020-11-26 01:04:33本文实例分析了python删除指定类型(或非指定)的文件用法。分享给大家供大家参考。具体如下:如下,删除目录下非源码文件123456789101112131415import osimport stringdef del_files(dir,topdown=True):for root, ... -
python删除文件_python删除指定类型(或非指定)的文件实例详解
2020-11-25 23:36:06本文实例分析了python删除指定类型(或非指定)的文件用法。分享给大家供大家参考。具体如下:如下,删除目录下非源码文件import osimport stringdef del_files(dir,topdown=True):for root, dirs, files in os.walk... -
python删除指定类型(或非指定)的文件实例详解
2020-12-23 16:44:44本文实例分析了python删除指定类型(或非指定)的文件用法。分享给大家供大家参考。具体如下: 如下,删除目录下非源码文件 import os import string def del_files(dir,topdown=True): for root, dirs, files in... -
python os.wolk()的使用
2020-08-30 15:27:58os.walk(top, topdown = True, onerror = None, followlinks = False) 文件结构 结果分析 1,先从根目录进行遍历,读取跟目录的文件夹和文件。 2,以根目录第一个子目录为新的根目录,读取其文件夹和文件。 3,再以... -
simplestroke:检测鼠标手势-源码
2021-02-19 14:55:02这样就可以在按下鼠标按钮的同时从窗口管理器开始简单笔划,并在释放它之后开始手势分析。 simplestroke打印检测到的手势的名称(如果有)。 然后可以在简单的shell脚本中使用输出来执行命令。 手势 支持以下手势... -
【极客日常】python的os.walk方法源码分析
2020-11-15 15:56:19因此本文采用源码分析的方式,讲述os.walk的机理,让大家对于这个方法有更加深入的理解。 以python3为例,os.walk方法的源码如下: def walk(top, topdown=True, onerror=None, followlinks=False): """Directory ...