profiling 与性能优化总结

一、 背景

在MegEngine imperative runtime的早期开发中,我们面临着一些的性能优化问题。除了一些已知需要重构的地方(早期设计时为了开发效率而牺牲性能的妥协),还存在一些未知的性能问题需要用profiler进行观测和分析才能发现。MegEngine的imperative runtime是一个由Python和C/C++编写的模块,对于这类程序,各种profiler多到令人眼花缭乱。在调研各种profiler的过程中,我们也踩了不少的坑,比如发现两个profiler对同一个程序的profiling结果差异巨大,我们起初怀疑其中一个profiler的准确性有问题,最后发现是两者的观测对象不同,其中一个只profiling程序的CPU time,而另一个profiling wall clock time。虽然一些这样的信息在文档的某些角落里能够找到,但很多使用者可能在踩了坑之后才会注意到。如果一开始能找到一篇介绍各种profiler的特点、优势、不足和使用场景的文章,我们可能会节省不少时间。

因此本文尝试对这些经验进行总结,希望能够给初次使用这些工具的读者一些参考。性能优化是一个十分广泛的话题,它涉及CPU、内存、磁盘、网络等方面,本文主要关注Python及C/C++ 拓展程序在CPU上的性能优化,文章主要围绕着下面三个问题展开:

  • Python及C/C++ 拓展程序的常见的优化目标有哪些
  • 常见工具的能力范围和局限是什么,给定一个优化目标我们应该如何选择工具
  • 各种工具的使用方法和结果的可视化方法

除此之外,本文还会介绍一些性能优化中需要了解的基本概念,并在文章末尾以MegEngine开发过程中的一个性能优化的实际案例来展示一个优化的流程。

二、 基本概念

本节介绍性能优化中的一些基础概念:

2.1 wall clock time, CPU time 和 off-CPU time

衡量程序性能最直接的标准就是程序的运行时间,但仅仅知道程序的运行时间很难指导我们如何把程序优化地更快,我们想要更进一步地了解这段时间之内到底发生了什么。

Linux系统上的time命令能够告诉我们一些粗糙的信息,我们在命令行里输出下面的命令来测量检查某个文件的CRC校验码的运行时间:
time cksum \<some_file\>
以我的电脑(MacBook Pro 2018)为例,得到了以下输出:
8.22s user 1.06s system 96% cpu 9.618 total
这段文字告诉了我们时间都花在了哪里:

  • 总时间 9.618s
  • user时间 8.22s
  • system时间 1.06s

其中user和system的含义是user CPU time和system CPU time, 之所以会把CPU的执行时间分为两个部分,是因为程序在运行时除了执行程序本身代码和一些库的代码,还会调用操作系统提供的函数(即系统调用,程序运行系统调用时有更高的权限),因此程序运行时通常会处于两种状态: 用户态和内核态: 内核态指的是CPU在运行系统调用时的状态,而用户态就是CPU运行非系统调用(即用户自己的代码或一些库)时的状态。

因此上面提到的user CPU time指的是用户态所花费的时间,而system CPU time指的是内核态花费的时间。

我们发现user CPU time + system CPU time = 8.22s + 1.06s = 9.28s并不等于总时间9.618s,这是因为这条命令执行的时间内,程序并不是总是在CPU上执行,还有可能处于睡眠、等待等状态,比如等待文件从磁盘加载到内存等。这段时间既不算在user CPU time也不算在 system CPU time内。我们把程序在CPU上执行的时间(即user CPU time + system CPU time)称为CPU time (或on-CPU time), 程序处于睡眠等状态的时间称为off-CPU time (or blocked time),程序实际运行的时间称为wall clock time(字面意思是墙上时钟的时间,也就是真实世界中流逝的时间),对于一个给定的线程: wall clock time = CPU time + off-CPU time。

通常在计算密集型(CPU intensive)的任务中CPU time会占据较大的比重,而在I/O密集型(I/O intensive)任务中off-CPU time会占据较大的比重。搞清楚CPU time和off-CPU time的区别对性能优化十分重要,比如某个程序的性能瓶颈在off-CPU time上,而我们选择了一个只观测CPU time的工具,那么很难找到真正的性能瓶颈,反之亦然。

2.2 性能观测工具

我们知道了一个线程执行过程中的CPU time 和 off-CPU time,如果要对程序的性能进行优化,这些还远远不够,我们需要进一步知道CPU time的时间段内,CPU上到底发生了哪些事情、这些事情分别消耗了多少时间、在哪里导致了线程被block住了、分别block了多久等。我们需要性能观测工具来获得这些详细的信息。通常情况下我们也将称这种观测工具称为profiler。

不同的观测对象对应着不同的profiler,仅就CPU而言,profiler也数不胜数。

按照观测范围来分类,CPU上的profiler大致可以分为两大类: 进程级(per-process, 某些地方也叫做应用级)和系统级(system wide),其中:

  • 进程级只观测一个进程或线程上发生的事情
  • 系统级不局限在某一个进程上,观测对象为整个系统上运行的所有程序

需要注意的是,某些工具既能观测整个系统也支持观测单个进程,比如perf,因此这样的工具同时属于两个类别。

按照观测方法来分类,大致可以分为event based和sampling based两大类。其中:

  • event based: 在一个指定的event集合上进行,比如进入或离开某个/某些特定的函数、分配内存、异常的抛出等事件。event based profiler在一些文章中也被称为tracing profiler或tracer
  • sampling based: 以某一个指定的频率对运行的程序的某些信息进行采样,通常情况下采样的对象是程序的调用栈

即使确定了我们优化的对象属于上述的某一个类别,仍然有更细粒度的分类。在选择工具之前要搞清楚具体的优化对象是什么,单个profiler一般无法满足我们所有的需求,针对不同的优化对象 (比如Python线程、C/C++线程等) 我们需要使用不同的profiler。并且,对于同一个优化对象,如果我们关注的信息不同,也可能需要使用不同的profiler。

2.3 Python进程模型

本文主要关注Python(包括C/C++拓展) 程序的优化,一个典型的Python和C/C++拓展程序的进程如下图所示:

一个Python进程必须包含一个Python主线程,可能包含若干个Python子线程和若干个C/C++子线程。因此我们进一步把优化对象细分为三类:

  • Python线程中的Python代码
  • Python线程中的C/C++拓展代码
  • C/C++线程

这里的Python线程具体指CPython解释器线程,而C/C++线程指不包含Python调用栈的C/C++线程。

三、 profiler的分类和选择

我们从以下两个角度对profiler进行刻画:

  • 是否支持profiling time、off-CPU time和wall clock time (CPU time + off-CPU time)
  • 是否支持profiling C/C++ stack
  • 是否能够从CPython解释器的调用栈中解析出Python调用栈

我们介绍将介绍6个profiler,分别为py-spy、cProfile、pyinstrument、perf、systemtap和eu-stack。为了方便大家进行选择,我们按照上面介绍的特征,把这些profiler分成了4类并总结在了下面的表格中 (其中✔、⚠、×分别表示支持、不完全支持和不支持):

表格中第一种是纯Python profiler,只能观测Python线程中Python函数的调用栈,适合优化纯Python代码时使用,本文将介绍CPr

  • 24
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值