有没有办法对多次执行的代码块进行计时?

Posted

技术标签:

【中文标题】有没有办法对多次执行的代码块进行计时?【英文标题】:Is there a way to time a block of code that is executed many times? 【发布时间】:2021-05-07 08:15:23 【问题描述】:

给定一个类似的函数:

do_something_big():
  do_1()
  do_2()
  do_3()

很容易获得运行do_something_big()do_1() 的单个实例所需的时间。但假设我有:

for _ in range(100000):
  do_something_big()

有没有一种简单的方法可以计算 100,000 do_1 花了多长时间?做到这一点并不难——您只需为每个时间计时并更新一些全局状态以跟踪聚合时间。但是是否已经构建了一个实用程序来为我抽象这个?

【问题讨论】:

您可以使用timeit 进行迭代计时 是的,我看过了 - 问题是它适用于传入的表达式;在这种情况下这行不通(据我所知)你能举一个例子说明你在do_1() 周围添加的内容,如果可行的话? 您需要通过运行 do_something_big 来为 do_1,do_2 的 100,000 次试验争取时间? 是的,如上例所示。 【参考方案1】:

我建议使用例如个人资料。这个包将对代码中的每个函数进行计时,并以良好的格式输出:

import cProfile

pr = cProfile.Profile()
pr.enable()

run_your_program_here()

pr.disable()
pr.print_stats(sort='tottime')

最后一行中的 sort 参数将根据您给定的选项对输出进行排序。你可以阅读更多关于它here:

ncalls – 函数/方法被调用了多少次(如果相同的函数/方法被递归调用,那么 ncalls 有两个值,例如 120/20,其中第一个是真实调用次数,第二是直接调用次数)

tottime – 以秒为单位的总时间,不包括其他函数/方法的时间

percall – 执行函数的平均时间(每次调用)

cumtime – 以秒为单位的总时间包括它调用的其他函数的时间

percall – 与之前的 percall 类似,但是这个包括网络延迟、线程休眠等……

在您的情况下,我会使用“tottime”并查看 do_1 花费了多少时间。

【讨论】:

这是一个合理的做法;我试过了,但对另一个分析库 pyinstrument 有更好的体验。无论哪种方式,这似乎都是最好的选择。 感谢 Rollie,很高兴为您解决了问题! @Rollie 也许您可以使用pyinstrument 为后代发布答案?【参考方案2】:

这是一个 decorator 作为您正在寻找的实用程序。灵感来自here 和here。

我们创建了一个装饰器@timeit。我们使用这个带有可选n 参数的装饰器来注释我们的方法。当达到n 调用时,打印此方法的时间度量,否则在每次调用时打印。

from functools import wraps
from time import time
from collections import defaultdict
timedata = defaultdict(lambda : (0, 0.0))
    
def timeit(*decArgs, **decKw):
    def _timeit(func):
        @wraps(func)
        def timed(*args, **kw):
            name = func.__name__.upper()
            n = decKw.get('n', 1)
            if timedata[name][0] >= n:
                return func(*args, **kw)

            ts = time()
            result = func(*args, **kw)
            te = time()
            duration = int((te - ts) * 1000)
            
            cnt, ave = timedata[name]
            cnt += 1
            timedata[name] = (cnt, ((cnt-1) * ave + duration) / cnt)
            if n == cnt:
                print(f'name:30s: [n:6dx] -> ave*cnt:7.3fms (ave:6.3fms ave)')
                timedata[name] = (0, 0.0)
            return result
        return timed
    return _timeit

用法:

from random import random

@timeit(n=100)
def do_1():
    sleep(random()/1000)

@timeit(n=3)
def do_something_big():
    for _ in range(200):
        do_1()

for _ in range(5):
    do_something_big()

输出:

DO_1                          : [   100x] ->  19.192ms ( 0.192ms ave)
DO_1                          : [   100x] ->  23.232ms ( 0.232ms ave)
DO_1                          : [   100x] ->  24.242ms ( 0.242ms ave)
DO_1                          : [   100x] ->  18.182ms ( 0.182ms ave)
DO_1                          : [   100x] ->  14.141ms ( 0.141ms ave)
DO_1                          : [   100x] ->  23.232ms ( 0.232ms ave)
DO_SOMETHING_BIG              : [     3x] -> 393.000ms (131.000ms ave)
DO_1                          : [   100x] ->  27.273ms ( 0.273ms ave)
DO_1                          : [   100x] ->  22.222ms ( 0.222ms ave)
DO_1                          : [   100x] ->  21.212ms ( 0.212ms ave)
DO_1                          : [   100x] ->  24.242ms ( 0.242ms ave)

【讨论】:

问题需要通过运行 do_something_big 为 do_1() 找到时间,这如何计算 do_1 时间? @GirishSrivatsa 我已经更新了示例代码,希望能解决您的问题。对于比这更复杂的事情,@eligolf 的cProfile 回答似乎是您最好的选择 是的,这就是我想要的,以已经构建的形式。如果我手动推出解决方案,这就是我希望实现它的方式,但我认为正确的方法毕竟是分析。【参考方案3】:

这是我在互联网上找到的https://realpython.com/python-timer/: 您将为 Python 计时器添加可选名称。您可以将该名称用于两种不同的目的:

稍后在您的代码中查找经过的时间 累加同名定时器 要为 Python 计时器添加名称,您需要对 timer.py 进行另外两项更改。首先,Timer 应该接受名称作为参数。其次,当计时器停止时,应将经过的时间添加到 .timers 中:

类计时器: timers = dict()

def __init__(
    self,
    name=None,
    text="Elapsed time: :0.4f seconds",
    logger=print,
):
    self._start_time = None
    self.name = name
    self.text = text
    self.logger = logger

    # Add new named timers to dictionary of timers
    if name:
        self.timers.setdefault(name, 0)

# Other methods are unchanged

def stop(self):
    """Stop the timer, and report the elapsed time"""
    if self._start_time is None:
        raise TimerError(f"Timer is not running. Use .start() to start it")

    elapsed_time = time.perf_counter() - self._start_time
    self._start_time = None

    if self.logger:
        self.logger(self.text.format(elapsed_time))
    if self.name:
        self.timers[self.name] += elapsed_time

    return elapsed_time

请注意,在将新的 Python 计时器添加到 .timers 时,您会使用 .setdefault()。这是一个很棒的功能,仅当名称尚未在字典中定义时才设置值。如果 name 已在 .timers 中使用,则该值保持不变。这允许您累积多个计时器:

>>> from timer import Timer
>>> t = Timer("accumulate")
>>> t.start()

>>> t.stop()  # A few seconds later
Elapsed time: 3.7036 seconds
3.703554293999332

>>> t.start()

>>> t.stop()  # A few seconds later
Elapsed time: 2.3449 seconds
2.3448921170001995

>>> Timer.timers
'accumulate': 6.0484464109995315

【讨论】:

以上是关于有没有办法对多次执行的代码块进行计时?的主要内容,如果未能解决你的问题,请参考以下文章

R语言使用Repeat函数多次执行代码块内的语句,实现循环执行任务的功能:repeat没有提供任何检查条件,所以编码者必须给出退出重复循环的条件(一般使用if和break)

Java的静态块和初始化块分别何时执行?有啥区别?

JAVA篇之------代码块

构造代码块局部代码块和静态代码块的定义作用和区别

代码块(初始化块)

静态代码块与非静态代码块