Concurrency In Python 简明教程

Benchmarking and Profiling

在本章中,我们了解基准测试和性能测量工具如何帮助解决性能问题。

假设我们编写了一个代码,它也给出了期望的结果,但是如果我们希望更快地运行此代码,因为需求已经改变,该怎么办。在这种情况下,我们需要找出我们代码的哪些部分正在拖慢整个程序。在这种情况下,基准测试和性能测量工具可能会有所帮助。

What is Benchmarking?

基准测试的目标是通过与标准进行比较来评估一些东西。然而,这里出现的一个问题是什么是基准测试,以及为什么我们需要它来进行软件编程。对代码进行基准测试表示代码执行速度有多快以及瓶颈在哪里。进行基准测试的一个主要原因是它可以优化代码。

How does benchmarking work?

如果我们讨论基准测试的工作原理,我们需要从将整个程序作为当前状态之一进行基准测试开始,然后我们可以组合微基准测试,然后将程序分解成更小的程序。为了找到我们程序中的瓶颈并对其进行优化。换句话说,我们可以将其理解为将大而困难的问题分解成一系列更小和更简单的问题,以便对它们进行优化。

Python module for benchmarking

在 Python 中,我们有一个用于基准测试的默认模块,称为 timeit 。借助 timeit 模块,我们可以在主程序中测量小型 Python 代码执行所需的时间。

Example

在以下 Python 脚本中,我们导入 timeit 模块,它进一步测量执行两个函数 - functionAfunctionB - 所花费的时间:

import timeit
import time
def functionA():
   print("Function A starts the execution:")
   print("Function A completes the execution:")
def functionB():
   print("Function B starts the execution")
   print("Function B completes the execution")
start_time = timeit.default_timer()
functionA()
print(timeit.default_timer() - start_time)
start_time = timeit.default_timer()
functionB()
print(timeit.default_timer() - start_time)

运行上述脚本后,我们将获得如下所示的两个函数的执行时间。

Output

Function A starts the execution:
Function A completes the execution:
0.0014599495514175942
Function B starts the execution
Function B completes the execution
0.0017024724827479076

Writing our own timer using the decorator function

在 Python 中,我们可以创建我们自己的计时器,它们的行为会像 timeit 模块一样。借助 decorator 函数即可完成此操作。以下是自定义计时器的示例 −

import random
import time

def timer_func(func):

   def function_timer(*args, **kwargs):
   start = time.time()
   value = func(*args, **kwargs)
   end = time.time()
   runtime = end - start
   msg = "{func} took {time} seconds to complete its execution."
      print(msg.format(func = func.__name__,time = runtime))
   return value
   return function_timer

@timer_func
def Myfunction():
   for x in range(5):
   sleep_time = random.choice(range(1,3))
   time.sleep(sleep_time)

if __name__ == '__main__':
   Myfunction()

上述 python 脚本有助于导入随机时间模块。我们创建了 timer_func() 装饰器函数。它有内部的 function_timer() 函数。现在,嵌套函数将在调用通过的函数之前获取时间。然后,它等待函数返回并获取结束时间。通过这种方式,我们最终可以让 Python 脚本打印执行时间。该脚本会生成如下所示的输出。

Output

Myfunction took 8.000457763671875 seconds to complete its execution.

What is profiling?

有时,程序员想要测量某些属性,比如程序的内存使用量、时间复杂度或特定指令的使用情况,以测量程序的实际性能。此类程序测量称为分析。分析使用动态程序来进行此类测量。

在后续部分中,我们将会了解用于分析的不同 Python 模块。

cProfile – the inbuilt module

cProfile 是用于分析的 Python 内置模块。该模块是开销合理的 C 扩展,这使其适用于分析运行时间较长的程序。运行它后,它会记录所有函数和执行时间。它非常强大,但有时有点难以解释和使用。在以下示例中,我们在下面代码中使用了 cProfile −

Example

def increment_global():

   global x
   x += 1

def taskofThread(lock):

   for _ in range(50000):
   lock.acquire()
   increment_global()
   lock.release()

def main():
   global x
   x = 0

   lock = threading.Lock()

   t1 = threading.Thread(target=taskofThread, args=(lock,))
   t2 = threading.Thread(target= taskofThread, args=(lock,))

   t1.start()
   t2.start()

   t1.join()
   t2.join()

if __name__ == "__main__":
   for i in range(5):
      main()
   print("x = {1} after Iteration {0}".format(i,x))

上述代码保存在 thread_increment.py 文件中。现在,在命令行中使用 cProfile 运行代码,如下所示 −

(base) D:\ProgramData>python -m cProfile thread_increment.py
x = 100000 after Iteration 0
x = 100000 after Iteration 1
x = 100000 after Iteration 2
x = 100000 after Iteration 3
x = 100000 after Iteration 4
      3577 function calls (3522 primitive calls) in 1.688 seconds

   Ordered by: standard name

   ncalls tottime percall cumtime percall filename:lineno(function)

   5 0.000 0.000 0.000 0.000 <frozen importlib._bootstrap>:103(release)
   5 0.000 0.000 0.000 0.000 <frozen importlib._bootstrap>:143(__init__)
   5 0.000 0.000 0.000 0.000 <frozen importlib._bootstrap>:147(__enter__)
   … … … …

从上述输出中,清楚的是 cProfile 打印出已调用的所有 3577 个函数,以及占用每个函数的时间和调用次数。以下是我们在输出中获得的列 −

  1. ncalls − 这是进行的调用次数。

  2. tottime − 这是总共在给定函数中所花的时间。

  3. percall − 它是 tottime 除以 ncalls 的商。

  4. cumtime − 这是在此函数和所有子函数中所花的时间。即使对于递归函数来说,它也是准确的。

  5. percall − 它是 cumtime 除以原始调用次数的商。

  6. filename:lineno(function) − 它基本上提供每个函数的相应数据。