调试慢程序;从中间重启

时间:2011-08-30 13:55:24

标签: python debugging

我有一个计算速度慢的程序,我希望调试算法。现在总是重新运行所有内容非常繁琐,我宁愿从程序中间重新启动。你能想到一些聪明的方法来实现这个目标吗?

第一个模糊的想法是定义检查点(我在其中进行函数调用),其中我用pickle和/或sqlite保存本地和大数据(sqlite能够检查中间数据)。后来我可以尝试调用程序告诉它在特定检查点重新启动。但是,为了这个目的,我不想在检查点之间拆分所有代码块。

有人聪明地知道如何解决这个调试问题吗?

6 个答案:

答案 0 :(得分:6)

使您的程序更加模块化。理想情况下,主要代码块应该类似于

import config
import my_numerics
import post_processing

my_numerics.configure(config.numerics)
values = my_numerics.run()

post_processing.run(values, config.post_processing)

你明白了。然后很容易制作一个“模拟”数字对象,它返回预先计算的数据,并将其传递给后处理。


编辑:我还是不明白。以下是您的问题的准确伪代码吗?

for _ in range(lots):
    do_slow_thing_one()

for _ in range(many):
    do_slow_thing_two()

for _ in range(lots_many)
    do_slow_thing_three()

也就是说,你想在第三个循环开始时中断运行中途(不是在结尾)中断数字,而不必重新运行前两个?

如果是这样,即使循环中不包含太多代码,也应该将设计模块化:

input_data = np.load(some_stuff)
stage_one = do_thing_one(input_data)
stage_two = do_thing_two(stage_one)
stage_three = do_thing_three(stage_two)

第一种方法是通过隐式接口在不同阶段之间传输数据;即,局部变量字典。这很糟糕,因为您尚未定义正在使用哪些变量,因此您无法模拟它们以进行测试/调试。第二种方式定义了函数之间的(基本)接口。您现在不再关心do_thing_one做什么,只要它需要一些输入数据并返回一些输出数据。这意味着要调试do_thing_three,您只需执行

stage_two = np.load(intermediate_stuff)
stage_three = do_thing_three(stage_two)

只要stage_two中的数据格式正确,它的来源无关紧要。

答案 1 :(得分:2)

单元测试

这就是单元测试存在的原因。尝试使用小型“示例数据”pyunit,或使用doctest获取非常简单的功能。

迷你测试程序

如果由于某种原因你确实需要互动性,我通常会编写一个互动的迷你程序作为有效的单元测试。

def _interactiveTest():
    ...
    import code
    code.interact(local=locals())

if __name__=='__main__':
    _interactiveTest()

如果您只测试特定部分,您通常可以忽略加载主程序的大块;根据需要调整您的体系结构,以避免初始化程序中不需要的部分。这就是人们可能会说“使你的程序更加模块化”的原因,这就是模块化的意思:程序的小块是独立的,让你重复使用它们(或者在这种情况下)单独加载它们。

在程序中调用口译员

您也可以在程序中的任何位置下载到翻译并传入当地人(如上所示)。这有点像“穷人的调试器”,但我发现它足够有效。 =)

单片算法

去过那里,做到了。有时您的工作流程无法进一步模块化,事情开始变得笨拙。

你制作检查站的直觉是非常好的,与我使用的相同:如果你在翻译环境中工作,或者嵌入翻译,你就不必经常处理这个问题,就像你刚才所说的那样你的脚本。序列化数据可以工作,但它会带来从磁盘读取和写入的大量开销;您希望数据集保留在内存中。然后你可以做类似test1 = algorithm(data), test2 = algorithm(data)的事情(假设你的算法不是就地算法;如果是,请在每次测试之前使用copy-on-write或复制数据结构)。

如果您在尝试以上所有内容后仍然遇到问题,那么或许您要么:

  • 使用您的真实数据集;你应该只是一个较小的原型测试数据集!
  • 使用效率低下的算法。

作为最后的手段,您可以对代码进行分析以找到瓶颈。

其他

可能有强大的python调试器。我认为Eclipse有一个。

另外,我个人避免reload <modulename>,我总是发现它比解决问题更令人头痛。

答案 2 :(得分:2)

Joblib以非常透明的方式处理结果缓存。这是一个例子,来自他们的文档:

>>> from joblib import Memory
>>> mem = Memory(cachedir='/tmp/joblib')
>>> import numpy as np
>>> a = np.vander(np.arange(3))
>>> square = mem.cache(np.square)
>>> b = square(a)
________________________________________________________________________________
[Memory] Calling square...
square(array([[0, 0, 1],
       [1, 1, 1],
       [4, 2, 1]]))
___________________________________________________________square - 0.0s, 0.0min

>>> c = square(a)
>>> # The above call did not trigger an evaluation because the result is cached

计算结果会自动保存在磁盘上,因此Joblib可能会满足您的需求。

答案 3 :(得分:1)

谷歌搜索指向我CryoPID,如果您在基于Linux的系统上进行开发,这可能会起作用。它声称能够暂停进程并将其保存到文件中,然后稍后重新启动它,即使在另一台计算机上也是如此。我没有测试过它。

答案 4 :(得分:0)

来自BuildBot的想法可能有用。它使用python的reload()重新加载更改模块,然后以半聪明的方式将状态从旧对象移动到新对象。

buildbot进程始终在运行,但可以发出信号从外部重新加载,在这种情况下会发生这种情况。

因此,如果您将算法的中间结果存储在对象中(类似于VTK减少计算的内容),您可以重新加载并重新创建算法对象,让它们重新加载旧数据然后写一些如果python模块实际上发生了变化,则重新运行这些对象的计算的逻辑。

这样,只要磁盘上的文件发生变化,您就可以重新加载进程。请注意,如果存在语法错误或运行时错误,则可能需要重新启动(除非您可以尝试传递并回滚到旧对象)。

所以,是的,需要检查站。但无论如何,拥有这样一个框架可能并不坏。 :)

实际上,只是模块化步骤将允许您将数据缓存到磁盘。这可能会解决这个问题。它肯定会对测试有所帮助,就像@katrielalex说的那样。

答案 5 :(得分:0)

我想到了这一点,最后我写下了我第一次只是模糊的想法。建议重新设计仍然会告诉如何跳过块,加载缓存数据等:

class DebugCheckpoints:
    def __init__(self, data, start):
        self.checkpoint_passed=False
        self.start=start
        self.data=data

    def __call__(self, variables):
        return Checkpoint(self, variables.split())


class CheckpointNotReached(Exception): pass


class Checkpoint:
    def __init__(self, debug_checkpoints, variables):
        self.variables=variables
        self.debug_checkpoints=debug_checkpoints

    def tag(self, tag_name):
        if self.debug_checkpoints.checkpoint_passed or \
           tag_name==self.debug_checkpoints.start:
            self.debug_checkpoints.checkpoint_passed=True
        else:
            raise CheckpointNotReached()

    def __enter__(self):
        return self

    def __exit__(self,exc_type, exc_val, exc_tb):
        if exc_type==CheckpointNotReached: # check if the context was supposed to be skipped
            for v in self.variables:
                globals()[v]=self.debug_checkpoints.data[v] # load globals from data
            return True
        else:
            for v in self.variables:
                self.debug_checkpoints.data[v]=globals()[v] # save globals to data
            return False

#------------------------------------------------------------------------------
data={"x":1, "w":4} # this is supposed to be any persistent dict
checkpoint=DebugCheckpoints(data, start="B") # start from B, skip block A but still load x and w from data

with checkpoint("x w") as c: # variable x and w is to be loaded
    c.tag("A") # this will force cancellation of this block, but x and w will be loaded from data
    x=1
    w=4
    print("Doing A")

with checkpoint("y") as c:
    c.tag("B") # as the start is B, this tag will no cancel this block
    y=2
    print("Doing B")

with checkpoint("z") as c:
    c.tag("C")
    z=3
    print("Doing C")

print(checkpoint.data)
print(x,y,z,w)

这是一个简单的框架,可以将检查点引入代码而无需编写太多内容。因为仅为每个小步骤定义数百个一次性函数可能会编码恐怖,而且函数中的变量是局部的(只需将每5行代码放入一个函数中)。我不想从块中返回所有变量并声明一切全局(这也是装饰器破解实现检查点框架的原因)。

也许我在尝试案例时重新设计了一些电话,但我认为这是一个好的开始。不确定什么应该进入with行和块内部(如.tag)。我没有设法将检查点例外放入__enter__