|
| 1 | +此教程会介绍如何使用Python的cProfile包,与Python库yep,google perftools来运行性能分析(Profiling)与调优。 |
| 2 | + |
| 3 | +运行性能分析可以让开发人员科学的,有条不紊的对程序进行性能优化。性能分析是性能调优的基础。因为在程序实际运行中,真正的瓶颈可能和程序员开发过程中想象的瓶颈相去甚远。 |
| 4 | + |
| 5 | +性能优化的步骤,通常是循环重复若干次『性能分析 --> 寻找瓶颈 ---> 调优瓶颈 --> 性能分析确认调优效果』。其中性能分析是性能调优的至关重要的量化指标。 |
| 6 | + |
| 7 | +Paddle提供了Python语言绑定。用户使用Python进行神经网络编程,训练,测试。Python解释器通过`pybind`和`swig`调用Paddle的动态链接库,进而调用Paddle C++部分的代码。所以Paddle的性能分析与调优分为两个部分: |
| 8 | + |
| 9 | +* Python代码的性能分析 |
| 10 | +* Python与C++混合代码的性能分析 |
| 11 | + |
| 12 | + |
| 13 | +## Python代码的性能分析 |
| 14 | + |
| 15 | +### 生成性能分析文件 |
| 16 | + |
| 17 | +Python标准库中提供了性能分析的工具包,[cProfile](https://docs.python.org/2/library/profile.html)。生成Python性能分析的命令如下: |
| 18 | + |
| 19 | +```bash |
| 20 | +python -m cProfile -o profile.out main.py |
| 21 | +``` |
| 22 | + |
| 23 | +其中`-o`标识了一个输出的文件名,用来存储本次性能分析的结果。如果不指定这个文件,`cProfile`会打印一些统计信息到`stdout`。这不方便我们进行后期处理(进行`sort`, `split`, `cut`等等)。 |
| 24 | + |
| 25 | +### 查看性能分析文件 |
| 26 | + |
| 27 | +当main.py运行完毕后,性能分析结果文件`profile.out`就生成出来了。我们可以使用[cprofilev](https://github.com/ymichael/cprofilev)来查看性能分析结果。`cprofilev`是一个Python的第三方库。使用它会开启一个HTTP服务,将性能分析结果以网页的形式展示出来。 |
| 28 | + |
| 29 | +使用`pip install cprofilev`安装`cprofilev`工具。安装完成后,使用如下命令开启HTTP服务 |
| 30 | + |
| 31 | +```bash |
| 32 | +cprofilev -a 0.0.0.0 -p 3214 -f profile.out main.py |
| 33 | +``` |
| 34 | + |
| 35 | +其中`-a`标识HTTP服务绑定的IP。使用`0.0.0.0`允许外网访问这个HTTP服务。`-p`标识HTTP服务的端口。`-f`标识性能分析的结果文件。`main.py`标识被性能分析的源文件。 |
| 36 | + |
| 37 | +访问对应网址,即可显示性能分析的结果。性能分析结果格式如下: |
| 38 | + |
| 39 | +```text |
| 40 | + ncalls tottime percall cumtime percall filename:lineno(function) |
| 41 | + 1 0.284 0.284 29.514 29.514 main.py:1(<module>) |
| 42 | + 4696 0.128 0.000 15.748 0.003 /home/yuyang/perf_test/.env/lib/python2.7/site-packages/paddle/v2/fluid/executor.py:20(run) |
| 43 | + 4696 12.040 0.003 12.040 0.003 {built-in method run} |
| 44 | + 1 0.144 0.144 6.534 6.534 /home/yuyang/perf_test/.env/lib/python2.7/site-packages/paddle/v2/__init__.py:14(<module>) |
| 45 | +``` |
| 46 | + |
| 47 | +每一列的含义是: |
| 48 | + |
| 49 | +| 列名 | 含义 | |
| 50 | +| --- | --- | |
| 51 | +| ncalls | 函数的调用次数 | |
| 52 | +| tottime | 函数实际使用的总时间。该时间去除掉本函数调用其他函数的时间 | |
| 53 | +| percall | tottime的每次调用平均时间 | |
| 54 | +| cumtime | 函数总时间。包含这个函数调用其他函数的时间 | |
| 55 | +| percall | cumtime的每次调用平均时间 | |
| 56 | +| filename:lineno(function) | 文件名, 行号,函数名 | |
| 57 | + |
| 58 | + |
| 59 | +### 寻找性能瓶颈 |
| 60 | + |
| 61 | +通常`tottime`和`cumtime`是寻找瓶颈的关键指标。这两个指标代表了某一个函数真实的运行时间。 |
| 62 | + |
| 63 | +将性能分析结果按照tottime排序,效果如下: |
| 64 | + |
| 65 | +```text |
| 66 | + 4696 12.040 0.003 12.040 0.003 {built-in method run} |
| 67 | + 300005 0.874 0.000 1.681 0.000 /home/yuyang/perf_test/.env/lib/python2.7/site-packages/paddle/v2/dataset/mnist.py:38(reader) |
| 68 | + 107991 0.676 0.000 1.519 0.000 /home/yuyang/perf_test/.env/lib/python2.7/site-packages/paddle/v2/fluid/framework.py:219(__init__) |
| 69 | + 4697 0.626 0.000 2.291 0.000 /home/yuyang/perf_test/.env/lib/python2.7/site-packages/paddle/v2/fluid/framework.py:428(sync_with_cpp) |
| 70 | + 1 0.618 0.618 0.618 0.618 /home/yuyang/perf_test/.env/lib/python2.7/site-packages/paddle/v2/fluid/__init__.py:1(<module>) |
| 71 | +
|
| 72 | +``` |
| 73 | + |
| 74 | +可以看到最耗时的函数是C++端的`run`函数。这需要联合我们第二节`Python与C++混合代码的性能分析`来进行调优。而`sync_with_cpp`函数的总共耗时很长,每次调用的耗时也很长。于是我们可以点击`sync_with_cpp`的详细信息,了解其调用关系。 |
| 75 | + |
| 76 | +```text |
| 77 | +Called By: |
| 78 | +
|
| 79 | + Ordered by: internal time |
| 80 | + List reduced from 4497 to 2 due to restriction <'sync_with_cpp'> |
| 81 | +
|
| 82 | +Function was called by... |
| 83 | + ncalls tottime cumtime |
| 84 | +/home/yuyang/perf_test/.env/lib/python2.7/site-packages/paddle/v2/fluid/framework.py:428(sync_with_cpp) <- 4697 0.626 2.291 /home/yuyang/perf_test/.env/lib/python2.7/site-packages/paddle/v2/fluid/framework.py:562(sync_with_cpp) |
| 85 | +/home/yuyang/perf_test/.env/lib/python2.7/site-packages/paddle/v2/fluid/framework.py:562(sync_with_cpp) <- 4696 0.019 2.316 /home/yuyang/perf_test/.env/lib/python2.7/site-packages/paddle/v2/fluid/framework.py:487(clone) |
| 86 | + 1 0.000 0.001 /home/yuyang/perf_test/.env/lib/python2.7/site-packages/paddle/v2/fluid/framework.py:534(append_backward) |
| 87 | +
|
| 88 | +
|
| 89 | +Called: |
| 90 | +
|
| 91 | + Ordered by: internal time |
| 92 | + List reduced from 4497 to 2 due to restriction <'sync_with_cpp'> |
| 93 | +``` |
| 94 | + |
| 95 | +通常观察热点函数间的调用关系,和对应行的代码,就可以了解到问题代码在哪里。当我们做出性能修正后,再次进行性能分析(profiling)即可检查我们调优后的修正是否能够改善程序的性能。 |
| 96 | + |
| 97 | + |
| 98 | + |
| 99 | +## Python与C++混合代码的性能分析 |
| 100 | + |
| 101 | +### 生成性能分析文件 |
| 102 | + |
| 103 | +C++的性能分析工具非常多。常见的包括`gprof`, `valgrind`, `google-perftools`。但是调试Python中使用的动态链接库与直接调试原始二进制相比增加了很多复杂度。幸而Python的一个第三方库`yep`提供了方便的和`google-perftools`交互的方法。于是这里使用`yep`进行Python与C++混合代码的性能分析 |
| 104 | + |
| 105 | +使用`yep`前需要安装`google-perftools`与`yep`包。ubuntu下安装命令为 |
| 106 | + |
| 107 | +```bash |
| 108 | +apt install libgoogle-perftools-dev |
| 109 | +pip install yep |
| 110 | +``` |
| 111 | + |
| 112 | +安装完毕后,我们可以通过 |
| 113 | + |
| 114 | +```bash |
| 115 | +python -m yep -v main.py |
| 116 | +``` |
| 117 | + |
| 118 | +生成性能分析文件。生成的性能分析文件为`main.py.prof`。 |
| 119 | + |
| 120 | +命令行中的`-v`指定在生成性能分析文件之后,在命令行显示分析结果。我们可以在命令行中简单的看一下生成效果。因为C++与Python不同,编译时可能会去掉调试信息,运行时也可能因为多线程产生混乱不可读的性能分析结果。为了生成更可读的性能分析结果,可以采取下面几点措施: |
| 121 | + |
| 122 | +1. 编译时指定`-g`生成调试信息。使用cmake的话,可以将CMAKE_BUILD_TYPE指定为`RelWithDebInfo`。 |
| 123 | +2. 编译时一定要开启优化。单纯的`Debug`编译性能会和`-O2`或者`-O3`有非常大的差别。`Debug`模式下的性能测试是没有意义的。 |
| 124 | +3. 运行性能分析的时候,先从单线程开始,再开启多线程,进而多机。毕竟如果单线程调试更容易。可以设置`OMP_NUM_THREADS=1`这个环境变量关闭openmp优化。 |
| 125 | + |
| 126 | +### 查看性能分析文件 |
| 127 | + |
| 128 | +在运行完性能分析后,会生成性能分析结果文件。我们可以使用[pprof](https://github.com/google/pprof)来显示性能分析结果。注意,这里使用了用`Go`语言重构后的`pprof`,因为这个工具具有web服务界面,且展示效果更好。 |
| 129 | + |
| 130 | +安装`pprof`的命令和一般的`Go`程序是一样的,其命令如下: |
| 131 | + |
| 132 | +```bash |
| 133 | +go get github.com/google/pprof |
| 134 | +``` |
| 135 | + |
| 136 | +进而我们可以使用如下命令开启一个HTTP服务: |
| 137 | + |
| 138 | +```bash |
| 139 | +pprof -http=0.0.0.0:3213 `which python` ./main.py.prof |
| 140 | +``` |
| 141 | + |
| 142 | +这行命令中,`-http`指开启HTTP服务。`which python`会产生当前Python二进制的完整路径,进而指定了Python可执行文件的路径。`./main.py.prof`输入了性能分析结果。 |
| 143 | + |
| 144 | +访问对应的网址,我们可以查看性能分析的结果。结果如下图所示: |
| 145 | + |
| 146 | + |
| 147 | + |
| 148 | + |
| 149 | +### 寻找性能瓶颈 |
| 150 | + |
| 151 | +与寻找Python代码的性能瓶颈类似,寻找Python与C++混合代码的性能瓶颈也是要看`tottime`和`cumtime`。而`pprof`展示的调用图也可以帮助我们发现性能中的问题。 |
| 152 | + |
| 153 | +例如下图中, |
| 154 | + |
| 155 | + |
| 156 | + |
| 157 | +在一次训练中,乘法和乘法梯度的计算占用2%-4%左右的计算时间。而`MomentumOp`占用了17%左右的计算时间。显然,`MomentumOp`的性能有问题。 |
| 158 | + |
| 159 | +在`pprof`中,对于性能的关键路径都做出了红色标记。先检查关键路径的性能问题,再检查其他部分的性能问题,可以更有次序的完成性能的优化。 |
| 160 | + |
| 161 | +## 总结 |
| 162 | + |
| 163 | +至此,两种性能分析的方式都介绍完毕了。希望通过这两种性能分析的方式,Paddle的开发人员和使用人员可以有次序的,科学的发现和解决性能问题。 |
0 commit comments