【问题标题】:cProfile adds significant overhead when calling numba jit functions调用 numba jit 函数时,cProfile 会增加大量开销
【发布时间】:2018-12-25 07:40:09
【问题描述】:

将纯 Python 无操作函数与以@numba.jit 修饰的无操作函数进行比较,即:

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 发生不良交互?

【问题讨论】:

    标签: python performance profiling numba cprofile


    【解决方案1】:

    当你创建一个 numba 函数时,你实际上创建了一个 numba Dispatcher 对象。这个对象“重新定向”对boring_numba 的“调用”到正确的(就类型而言)内部“jitted”函数。因此,即使您创建了一个名为 boring_numba 的函数 - 此函数并未被调用,所调用的是基于您的函数的编译函数

    您可以看到在分析Dispatcher 对象时调用了函数boring_numba(即使不是,调用的是CPUDispatcher.__call__)需要挂钩到当前线程状态并检查是否有一个分析器/跟踪器正在运行,如果“是”,它看起来就像调用了 boring_numba。这最后一步是产生开销的原因,因为它必须为 boring_numba 伪造一个“Python 堆栈框架”。

    有点技术性:

    当您调用 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,不确定这是否也适用于过去的版本。

    【讨论】:

      猜你喜欢
      • 2021-01-28
      • 1970-01-01
      • 2015-10-25
      • 2021-09-29
      • 2013-03-28
      • 2021-09-19
      • 1970-01-01
      • 2013-08-01
      • 2011-04-02
      相关资源
      最近更新 更多