如何进行 Python性能分析,你才能如鱼得水?

【编者按】本文作者为 Bryan Helmig,主要介绍 Python 应用性能分析的三种进阶方案。文章系国内 ITOM 管理平台 OneAPM 编译呈现。

我们应该忽略一些微小的效率提升,几乎在 97% 的情况下,都是如此:过早的优化是万恶之源。—— Donald Knuth

如果不先想想Knuth的这句名言,就开始进行优化工作,是不明智的。然而,有时你为了获得某些特性不假思索就写下了O(N^2) 这样的代码,虽然你很快就忘记它们了,它们却可能反咬你一口,给你带来麻烦:本文就是为这种情况而准备的。

本文会介绍用于快速分析Python程序的一些有用工具和模式。主要目标很简单:尽快发现问题,修复问题,并确认修复是行之有效的

编写一个测试

在教程开始前,要先写一个简单的概要测试来演示延迟。你可能需要引入一些最小数据集来重现可观的延迟。通常一或两秒的运行时间,已经足够在发现问题时,让你做出改进了。

此外,进行一些基础测试来确保你的优化不会修改缓慢代码的行为也是有必要的。在分析和调整时,你也可以多次运行这些测试,作为基准。

那么现在,我们来看第一个分析工具。

简单的计时器

计时器是简单、灵活的记录执行时间的方法。你可以把它放到任何地方,并且几乎没有副作用。自己创建计时器非常简单,并且可以根据你的喜好定制化。例如,一个简单的计时器可以这么写:

import time
def timefunc(f):
  def f_timer(args, *kwargs):
        start = time.time()
        result = f(args, *kwargs)
        end = time.time()
        print f.__name__, 'took', end - start, 'time'
        return result
     return f_timer

def get_number():
  for x in xrange(5000000):
      yield x

@timefunc
def expensive_function():
   for x in get_number():
        i = x ^ x ^ x
   return 'some result!'
# prints "expensive_function took 0.72583088875 seconds"
result = expensive_function()

当然,你可以用上下文管理器来增强它的功能,添加一些检查点或其他小功能:

import time

class timewith():
   def __init__(self, name=''):
        self.name = name
        self.start = time.time()    

   @property
   def elapsed(self):
     return time.time() - self.start    

   def checkpoint(self, name=''):
      print '{timer} {checkpoint} took {elapsed} seconds'.format(
            timer=self.name,
            checkpoint=name,
            elapsed=self.elapsed,
        ).strip()    

    def __enter__(self):
       return self    

    def __exit__(self, type, value, traceback):
        self.checkpoint('finished')
        pass

def get_number():
   for x in xrange(5000000):
      yield x

def expensive_function():
   for x in get_number():
        i = x ^ x ^ x
    return 'some result!'

# prints something like:
# fancy thing done with something took 0.582462072372 seconds
# fancy thing done with something else took 1.75355315208 seconds
# fancy thing finished took 1.7535982132 seconds
with timewith('fancy thing') as timer:
    expensive_function()
    timer.checkpoint('done with something')
    expensive_function()
    expensive_function()
    timer.checkpoint('done with something else')

# or directly
timer = timewith('fancy thing')
expensive_function()
timer.checkpoint('done with something')

有了计时器,你还需要进行一些“挖掘”工作。 封装一些更为高级的函数,然后确定问题根源之所在,进而深入可疑的函数,不断重复。当你发现运行特别缓慢的代码之后,修复它,然后进行测试以确认修复成功。

提示:不要忘了便捷的 timeit 模块!将它用于小段代码块的基准校验比实际测试更加有用。

  • 计时器的优点:容易理解和实施,也非常容易在修改前后进行对比,对于很多语言都适用。
  • 计时器的缺点:有时候,对于非常复杂的代码库而已太过简单,你可能会花更多的时间创建、替换样板代码,而不是修复问题!

内建分析器

内建分析器就好像大型枪械。虽然非常强大,但是有点不太好用,有时,解释和操作起来比较困难。

你可以点此阅读更多关于内建分析模块的内容,但是内建分析器的基本操作非常简单:你启用和禁用分析器,它能记录所有的函数调用和执行时间。接着,它能为你编译和打印输出。一个简单的分析器用例如下:

import cProfile

def do_cprofile(func):
  def profiled_func(args, *kwargs):
        profile = cProfile.Profile()
        try:
            profile.enable()
            result = func(args, *kwargs)
            profile.disable()
            return result
            finally:
            profile.print_stats()
        return profiled_func

def get_number():
   for x in xrange(5000000):
      yield x

@do_cprofile
def expensive_function():
   for x in get_number():
        i = x ^ x ^ x
   return 'some result!'

# perform profiling
result = expensive_function()

在上面代码中,控制台打印的内容如下:

         5000003 function calls in 1.626 seconds

   Ordered by: standard name
   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
   5000001    0.571    0.000    0.571    0.000 timers.py:92(get_number)
   1    1.055    1.055    1.626    1.626 timers.py:96(expensive_function)
   1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

如你所见,它给出了不同函数调用的详细数据。但是,它遗漏了一项关键信息:是什么原因,导致函数运行如此缓慢?

然而,这对于基础分析来说是个好的开端。有时,能够帮你尽快找到解决方案。我经常在开始调试过程时,把它作为基本测试,然后再深入测试某个不是运行缓慢,就是调用频繁的特定函数。

  • 内建分析器的优点:没有外部依赖,运行非常快。对于快速的概要测试非常有用。
  • 内建分析器的缺点:信息相对有限,需要进一步的调试;报告不太直观,尤其是对于复杂的代码库。

Line Profiler

如果内建分析器是大型枪械,line profiler就好比是离子炮。它非常的重量级且强大,使用起来也非常有趣。

在这个例子里,我们会用非常棒的kernprof line-profiler,作为 line_profiler PyPi包。为了方便使用,我们会再次用装饰器进行封装,同时也可以防止我们把它留在生产代码里(因为它比蜗牛还慢)。

try:
   from line_profiler import LineProfiler    

   def do_profile(follow=[]):
      def inner(func):
         def profiled_func(args, *kwargs):
            try:
                    profiler = LineProfiler()
                    profiler.add_function(func)
                    for f in follow:
                        profiler.add_function(f)
                    profiler.enable_by_count()
                    return func(args, *kwargs)
             finally:
                    profiler.print_stats()
         return profiled_func
       return inner

except ImportError:
  def do_profile(follow=[]):
    "Helpful if you accidentally leave in production!"
    def inner(func):
       def nothing(args, *kwargs):
          return func(args, *kwargs)
       return nothing
     return inner

def get_number():
   for x in xrange(5000000):
     yield x

@do_profile(follow=[get_number])
def expensive_function():
   for x in get_number():
        i = x ^ x ^ x
   return 'some result!'

result = expensive_function()

如果运行上面的代码,就会看到以下的报告:

Timer unit: 1e-06 s

File: test.py
Function: get_number at line 43Total time: 4.44195 s

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
43                                           def get_number():
44   5000001      2223313      0.4     50.1      for x in xrange(5000000):
45   5000000      2218638      0.4     49.9          yield x

File: test.py
Function: expensive_function at line 47Total time: 16.828 s

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
47                                           def expensive_function():
48   5000001     14090530      2.8     83.7      for x in get_number():
49   5000000      2737480      0.5     16.3          i = x ^ x ^ x
50         1            0      0.0      0.0      return 'some result!'

如你所见,这是一个非常详细的报告,能让你完全洞悉代码的运行情况。和内建的cProfiler不同,它能分析核心语言特性的耗时,比如循环或导入,并且给出不同代码行的耗时累计值。

这些细节能让我们更容易理解函数内部原理。 此外,如果需要研究第三方库,你可以将其导入,直接输到装饰器中。

提示:将测试函数封装为装饰器,再将问题函数作为参数传进去就好了!

  • Line Profiler 的优点:有非常直接和详细的报告。能够追踪第三方库里的函数。
  • Line Profiler 的缺点:因为系统开销巨大,会比实际执行时间慢一个数量级,所以不要用它进行基准测试。同时,它是外部工具。

结论和最佳方案

你应该使用简单的工具(比如计时器或内建分析器)对测试用例(特别是那些你非常熟悉的代码)进行基本检查,然后使用更慢但更加细致的工具,比如 line_profiler,深入检查函数内部。

十有八九,你会发现一个愚蠢的错误,比如在循环内重复调用,或是使用了错误的数据结构,消耗了90%的函数执行时间。在进行快速(且令人满意的)调整之后,问题就能得到解决。

如果你仍然觉得程序运行太过缓慢,然后开始进行对比属性访问(ttribute accessing)方法,或调整相等检查(equality checking)方法等晦涩的调整,你可能已经适得其反了。你应该考虑如下方法:

1.忍受缓慢或者预先计算/缓存
2.重新思考整个实施方法
3.使用更多的优化数据结构(通过 Numpy,Pandas等)
4.编写一个 C扩展

注意,优化代码会带来有罪恶感的快乐!寻找加速Python的合理方法很有趣,但是不要因为加速,破坏了本身的逻辑。易读的代码比运行速度更重要。实施缓存,往往是最简单的解决方法。

教程到此为止,希望你今后的Python性能分析能够如鱼得水!

PS: 点此查看代码实例。此外,点此学习如何如鱼得水地调试 Python 程序。

OneAPM 能帮你查看 Python 应用程序的方方面面,不仅能够监控终端的用户体验,还能监控服务器性能,同时还支持追踪数据库、第三方 API 和 Web 服务器的各种问题。想阅读更多技术文章,请访问 OneAPM 官方技术博客

本文转自 OneAPM 官方博客

原文地址:https://zapier.com/engineering/profiling-python-boss/

时间: 2024-10-02 02:11:21

如何进行 Python性能分析,你才能如鱼得水?的相关文章

Python 性能诊断及代码优化技巧

    程序代码的优化通常包含:减小代码的体积,提高代码的运行效率.这样可以让程序运行得更快.下面我们来具体谈谈 Python 代码优化常见技巧. 改进算法,选择合适的数据结构 一个良好的算法能够对性能起到关键作用,因此性能改进的首要点是对算法的改进.在算法的时间复杂度排序上依次是: O(1) -> O(lg n) -> O(n lg n) -> O(n^2) -> O(n^3) -> O(n^k) -> O(k^n) -> O(n!) 因此如果能够在时间复杂度上

用Python编写分析Python程序性能的工具的教程_python

虽然并非你编写的每个 Python 程序都要求一个严格的性能分析,但是让人放心的是,当问题发生的时候,Python 生态圈有各种各样的工具可以处理这类问题. 分析程序的性能可以归结为回答四个基本问题:     正运行的多快     速度瓶颈在哪里     内存使用率是多少     内存泄露在哪里 下面,我们将用一些神奇的工具深入到这些问题的答案中去.用 time 粗粒度的计算时间 让我们开始通过使用一个快速和粗暴的方法计算我们的代码:传统的 unix time 工具.   $ time pyth

《Python高性能编程》——第2章 通过性能分析找到瓶颈 2.1 高效地分析性能

第2章 通过性能分析找到瓶颈 读完本章之后你将能够回答下列问题 如何找到代码中速度和RAM的瓶颈? 如何分析CPU和内存使用情况? 我应该分析到什么深度? 如何分析一个长期运行的应用程序? 在CPython台面下发生了什么? 如何在调整性能的同时确保功能的正确? 性能分析帮助我们找到瓶颈,让我们在性能调优方面做到事半功倍.性能调优包括在速度上巨大的提升以及减少资源的占用,也就是说让你的代码能够跑得"足够快"以及"足够瘦".性能分析能够让你用最小的代价做出最实用的决定

《Python高性能编程》——2.14 确保性能分析成功的策略

2.14 确保性能分析成功的策略 性能分析需要一些时间和精力.如果你把需要测试的代码段跟你代码的主体分离,你会有一个更好的机会去了解你的代码.然后你可以用单元测试来保证正确性,你还可以传入精心编造的真实数据来测试算法的有效性. 记得关闭任何基于BIOS的加速器,因为它们只会混淆你的结果.Ian的笔记本电脑使用的Intel TurboBoost功能可以在温度足够低的时候将CPU暂时加至极速.这意味着低温时运行同一段代码的速度可能比高温时要快.你的操作系统也许还控制了时钟的速度--使用电池电源的笔记

Python性能提升之延迟初始化_python

所谓类属性的延迟计算就是将类的属性定义成一个property,只在访问的时候才会计算,而且一旦被访问后,结果将会被缓存起来,不用每次都计算.构造一个延迟计算属性的主要目的是为了提升性能 property 在切入正题之前,我们了解下property的用法,property可以将属性的访问转变成方法的调用. class Circle(object): def __init__(self, radius): self.radius = radius @property def area(self):

Linux系统下常见性能分析工具的使用

在前面的文章中,我简单介绍了影响linux性能的几个方面以及如何解决这些方面的问题,但是如何才能从系统上发现是某个方面或某几个方面出现问题了呢,这就需要使用linux系统提供的几个常用性能分析工具,下面就具体讲述这几个常用性能分析工具的使用. 1.vmstat命令 vmstat是Virtual Meomory Statistics(虚拟内存统计)的缩写,很多linux发行版本都默认安装了此命令工具,利用vmstat命令可以对操作系统的内存信息.进程状态.CPU活动等进行监视,不足之处是无法对某个

PHP 性能分析与实验:性能的微观分析

在上一篇文章中,我们从 PHP 是解释性语言.动态语言和底层实现等三个方面,探讨了 PHP 性能的问题.本文就深入到 PHP 的微观层面,我们来了解 PHP 在使用和编写代码过程中,性能方面,可能需要注意和提升的地方. 在开始分析之前,我们得掌握一些与性能分析相关的函数.这些函数让我们对程序性能有更好的分析和评测. 一.性能分析相关的函数与命令 1.1.时间度量函数 平时我们常用 time() 函数,但是返回的是秒数,对于某段代码的内部性能分析,到秒的精度是不够的.于是要用 microtime

MySQL性能分析和优化-part 1

MySQL性能优化 平时我们在使用MySQL的时候,怎么评估系统的运行状态,怎么快速定位系统瓶颈,又如何快速解决问题呢? 本文总结了多年来MySQL优化的经验,系统介绍MySQL优化的方法. OS性能分析 使用top观察top cpu/memory进程 ~ top top - 09:34:29 up 10 days, 20:11, 1 user, load average: 0.61, 0.59, 0.60 Tasks: 208 total, 1 running, 207 sleeping, 0

【译】几个Python性能优化技巧

问题描述 Python是一门非常酷的语言,因为很少的Python代码可以在短时间内做很多事情,并且,Python很容易就能支持多任务和多重处理.Python的批评者声称Python性能低效.执行缓慢,但实际上并非如此:尝试以下6个小技巧,可以加快Pytho应用程序. **1.关键代码可以依赖于扩展包**Python使许多编程任务变得简单,但是对于很关键的任务并不总是提供最好的性能.使用C.C++或者机器语言扩展包来执行关键任务能极大改善性能.这些包是依赖于平台的,也就是说,你必须使用特定的.与你