当调用numba jit函数时,cProfile会增加大量开销

时间:2018-07-17 14:37:52

标签: python performance profiling numba cprofile

将纯Python no-op函数与用@numba.jit装饰的no-op函数进行比较,即:

import numba

@numba.njit
def boring_numba():
    pass

def call_numba(x):
    for t in range(x):
        boring_numba()

def boring_normal():
    pass

def call_normal(x):
    for t in range(x):
        boring_normal()

如果使用%timeit进行计时,则会得到以下信息:

%timeit call_numba(int(1e7))
792 ms ± 5.51 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

%timeit call_normal(int(1e7))
737 ms ± 2.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

一切都完全合理; numba函数的开销很小,但不多。

但是,如果我们使用cProfile来分析此代码,则会得到以下信息:

cProfile.run('call_numba(int(1e7)); call_normal(int(1e7))', sort='cumulative')

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
     76/1    0.003    0.000    8.670    8.670 {built-in method builtins.exec}
        1    6.613    6.613    7.127    7.127 experiments.py:10(call_numba)
        1    1.111    1.111    1.543    1.543 experiments.py:17(call_normal)
 10000000    0.432    0.000    0.432    0.000 experiments.py:14(boring_normal)
 10000000    0.428    0.000    0.428    0.000 experiments.py:6(boring_numba)
        1    0.000    0.000    0.086    0.086 dispatcher.py:72(compile)

cProfile认为调用numba函数会产生大量开销。 这扩展到“真实”代码:我有一个函数,它简单地称为昂贵的计算(该计算由numba-JIT编译),并且cProfile报告说包装器函数占用了总时间的三分之一。

我不介意cProfile会增加一些开销,但是如果在增加开销的位置上存在很大的不一致,那不是很有帮助。有谁知道为什么会这样,是否有任何办法可以解决,和/或是否有其他可选的配置工具不会与numba交互不良?

1 个答案:

答案 0 :(得分:8)

创建numba函数时,实际上创建了numba Dispatcher对象。该对象将对“ boring_numba”的“调用”重定向到正确的(就类型而言)内部“被触发”功能。因此,即使您创建了一个名为boring_numba的函数-该函数不会被调用,所调用的是基于函数的 编译函数。

因此,您可以看到在对boring_numba对象进行概要分析时,调用了函数CPUDispatcher.__call__(即使未调用,称为Dispatcher)也需要连接到当前线程状态,并检查是否正在运行探查器/跟踪器,如果是,则使它看起来像boring_numba。这最后一步是造成开销的原因,因为它必须伪造“ Python堆栈框架”以用于boring_numba

更具技术性:

当您调用numba函数boring_numba时,实际上会调用Dispatcher_Call,它是call_cfunc的包装,这是主要的区别:当您有一个分析器运行用于处理探查器构成了函数调用的大部分(如果没有探查器/跟踪器,只需将if (tstate->use_tracing && tstate->c_profilefunc)分支与正在运行的else分支进行比较):

static PyObject *
call_cfunc(DispatcherObject *self, PyObject *cfunc, PyObject *args, PyObject *kws, PyObject *locals)
{
    PyCFunctionWithKeywords fn;
    PyThreadState *tstate;
    assert(PyCFunction_Check(cfunc));
    assert(PyCFunction_GET_FLAGS(cfunc) == METH_VARARGS | METH_KEYWORDS);
    fn = (PyCFunctionWithKeywords) PyCFunction_GET_FUNCTION(cfunc);
    tstate = PyThreadState_GET();
    if (tstate->use_tracing && tstate->c_profilefunc)
    {
        /*
         * The following code requires some explaining:
         *
         * We want the jit-compiled function to be visible to the profiler, so we
         * need to synthesize a frame for it.
         * The PyFrame_New() constructor doesn't do anything with the 'locals' value if the 'code's
         * 'CO_NEWLOCALS' flag is set (which is always the case nowadays).
         * So, to get local variables into the frame, we have to manually set the 'f_locals'
         * member, then call `PyFrame_LocalsToFast`, where a subsequent call to the `frame.f_locals`
         * property (by virtue of the `frame_getlocals` function in frameobject.c) will find them.
         */
        PyCodeObject *code = (PyCodeObject*)PyObject_GetAttrString((PyObject*)self, "__code__");
        PyObject *globals = PyDict_New();
        PyObject *builtins = PyEval_GetBuiltins();
        PyFrameObject *frame = NULL;
        PyObject *result = NULL;

        if (!code) {
            PyErr_Format(PyExc_RuntimeError, "No __code__ attribute found.");
            goto error;
        }
        /* Populate builtins, which is required by some JITted functions */
        if (PyDict_SetItemString(globals, "__builtins__", builtins)) {
            goto error;
        }
        frame = PyFrame_New(tstate, code, globals, NULL);
        if (frame == NULL) {
            goto error;
        }
        /* Populate the 'fast locals' in `frame` */
        Py_XDECREF(frame->f_locals);
        frame->f_locals = locals;
        Py_XINCREF(frame->f_locals);
        PyFrame_LocalsToFast(frame, 0);
        tstate->frame = frame;
        C_TRACE(result, fn(PyCFunction_GET_SELF(cfunc), args, kws));
        tstate->frame = frame->f_back;

    error:
        Py_XDECREF(frame);
        Py_XDECREF(globals);
        Py_XDECREF(code);
        return result;
    }
    else
        return fn(PyCFunction_GET_SELF(cfunc), args, kws);
}

我假设在执行cProfile-ing时,这些额外的代码(以防运行分析器)会降低功能。

不幸的是,当您运行探查器时,numba函数会增加太多的开销,但是如果您在numba函数中做任何实质性的事情,则减慢速度几乎可以忽略不计。 如果您还可以在numba函数中移动for循环,那么甚至更多。

如果您注意到numba函数(运行或不运行探查器)花费了太多时间,则您可能经常调用它。然后,您应该检查一下是否可以真正将循环移动到numba函数中,或者将包含该循环的代码包装到另一个numba函数中。

注意:所有这些都是(一点)猜测,我实际上没有使用调试符号构建numba并在运行探查器的情况下探查了C代码。但是,如果有一个探查器正在运行,那么大量的操作就显得很合理。而所有这些都假设numba 0.39,不确定是否也适用于过去的版本。

相关问题