通常,IPython 的%time、%timeit、%prun 和%lprun(如果安装了line_profiler)可以很好地满足我的分析需求。但是,当我尝试分析交互式驱动的计算时,即通过用户在 GUI 中的鼠标移动,出现了 tic-toc 类似功能的用例。我觉得在源代码中发送垃圾邮件tics 和tocs,而交互式测试将是揭示瓶颈的最快方法。我参加了 Eli Bendersky 的 Timer 课程,但并不完全满意,因为它需要我更改代码的缩进,这在某些编辑器中可能很不方便,并且会混淆版本控制系统。此外,可能需要测量不同函数中点之间的时间,这不适用于with 语句。在尝试了很多 Python 技巧之后,这是我发现效果最好的简单解决方案:
from time import time
_tstart_stack = []
def tic():
_tstart_stack.append(time())
def toc(fmt="Elapsed: %s s"):
print fmt % (time() - _tstart_stack.pop())
由于这是通过将开始时间推入堆栈来实现的,因此它可以在tics 和tocs 的多个级别上正常工作。它还允许更改 toc 语句的格式字符串以显示其他信息,我喜欢 Eli 的 Timer 类。
出于某种原因,我担心纯 Python 实现的开销,所以我也测试了一个 C 扩展模块:
#include <Python.h>
#include <mach/mach_time.h>
#define MAXDEPTH 100
uint64_t start[MAXDEPTH];
int lvl=0;
static PyObject* tic(PyObject *self, PyObject *args) {
start[lvl++] = mach_absolute_time();
Py_RETURN_NONE;
}
static PyObject* toc(PyObject *self, PyObject *args) {
return PyFloat_FromDouble(
(double)(mach_absolute_time() - start[--lvl]) / 1000000000L);
}
static PyObject* res(PyObject *self, PyObject *args) {
return tic(NULL, NULL), toc(NULL, NULL);
}
static PyMethodDef methods[] = {
{"tic", tic, METH_NOARGS, "Start timer"},
{"toc", toc, METH_NOARGS, "Stop timer"},
{"res", res, METH_NOARGS, "Test timer resolution"},
{NULL, NULL, 0, NULL}
};
PyMODINIT_FUNC
inittictoc(void) {
Py_InitModule("tictoc", methods);
}
这是针对 MacOSX 的,为了简洁起见,我省略了检查 lvl 是否超出范围的代码。虽然tictoc.res() 在我的系统上产生了大约 50 纳秒的分辨率,但我发现测量任何 Python 语句的抖动很容易在微秒范围内(在 IPython 中使用时更多)。此时,Python 实现的开销变得可以忽略不计,因此可以像 C 实现一样放心地使用它。
我发现tic-toc-方法的用处实际上仅限于执行时间超过 10 微秒的代码块。在此之下,需要像timeit 这样的平均策略才能获得忠实的测量结果。