cProfile добавляет значительные накладные расходы при вызове функций numba jit
Сравните чистую функцию Python no-op с функцией no-op, украшенной @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)
Все совершенно разумно; есть небольшая нагрузка на функцию нумба, но не очень.
Однако, если мы используем 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 ответ
Когда вы создаете функцию 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-In.
Немного прискорбно, что функция numba добавляет столько накладных расходов, когда вы запускаете профилировщик, но замедление будет практически незначительным, если вы сделаете что-то существенное в функции numba. Если бы вы также переместить for
цикл в функции Numba, то еще больше.
Если вы заметили, что функция numba (с запущенным или без использования профилировщика) занимает слишком много времени, вы, вероятно, вызываете ее слишком часто. Затем вы должны проверить, действительно ли вы можете переместить цикл внутри функции numba или обернуть код, содержащий цикл, в другую функцию numba.
Примечание: все это (немного) предположение, на самом деле я не собирал numba с символами отладки и не профилировал C-код на случай, если профилировщик работает. Однако количество операций, выполняемых профилировщиком, делает это весьма правдоподобным. И все это предполагает numba 0.39, не уверенный, относится ли это и к прошлым версиям.