从 Numba jitted 代码调用 Cython 函数

Calling Cython functions from Numba jitted code

我知道调用另一个 jitted 函数的 Numba-jitted 函数会识别这一点并自动使用快速 C 调用约定而不是通过 Python 对象层,因此避免高 Python 函数调用开销:

import numba

@numba.jit
def foo(x):
    return x**2

@numba.jit
def bar(x):
    return 4 * foo(x)   # this will be a fast function call

我的问题是如果我从 Numba 调用 Cython 函数是否也是如此。假设我有一个 Cython 模块,foo.pyx:

cpdef double foo(double x):
    return x**2

以及标准 Python 模块 bar.py:

import numba
import foo

@numba.jit
def bar(x):
    return 4 * foo.foo(x)   # will this be a fast function call?

Numba 会自动将 foo.foo 识别为 C 语言可调用函数,还是我需要通过设置 CFFI 包装器等方式手动告知它?

编辑: 经过进一步思考,从 Python 解释器的角度来看,Cython 函数只是标准的 "builtin" 函数。所以问题可以更笼统:Numba 是否优化了对内置函数和方法的调用以绕过 Python 调用开销?

numba 知道如何将一组有限的内置函数(来自 python 标准库和 numpy)转换为本机代码:

nopython 模式下,Numba 无法排除任何其他内容,因此求助于 objectmode,这要慢得多。

没有直接的方法将 cython 函数传递给 Numba 并使其在 nopython 模式下被识别。 Numba 确实有 cffi 的挂钩:

http://numba.pydata.org/numba-doc/latest/reference/pysupported.html#cffi

可以利用它来调用外部 C 代码,如果您可以在 C 级创建低级包装器,您可能可以装配以调用 cython;不过,我不是 100% 确定这是否可行。我写过这样做是为了从 Numba 调用 RMath 函数:

https://web.archive.org/web/20160611082327/https://www.continuum.io/blog/developer-blog/calling-c-libraries-numba-using-cffi

如果你走那条路,它可能有助于你入门。

可以在 nopython-numba:

中使用 Cython 的 cpdef/cdef-函数(但不能使用 def-函数)
  1. 步骤:cdef/cpdef 函数在 Cython 代码中必须是 marked as api
  2. 步骤:numba.extending.get_cython_function_address可用于获取cpdef-function的地址。
  3. 步骤:ctypes可用于从cpdef-function的地址创建CFunction,可用于numba-nopython代码。

继续阅读以获得更详细的解释。


即使 builtin-functions(PyCFunction,与 Cython 的 def 函数相同)是用 C 语言编写的,它们也没有可供 nopython-numba-代码.

例如 math 模块中的 acos 函数没有签名

`double acos(double)`

如人们所料,但 its signature

static PyObject * math_acos(PyObject *self, PyObject *args)

所以基本上为了调用这个函数,numba 需要从手头的 C-float 构建一个 Python-float,但这被 nopython=True.[=60= 禁止]

然而,Cythons cpdef-函数有点不同:它是一个围绕真实 cdef-函数的小包装器,其参数是原始的 C-types,如 [=36] =]、int等。这个 cdef 函数可以被 numba 使用,前提是它的地址是已知的。

Cython 提供了一种以可移植的方式查找 cdef 函数地址的方法:地址可以在 cython 化模块的属性 __pyx_capi__ 中找到。

然而,并非所有 cdefcpdef 函数都以这种方式公开,只有明确标记为 C-api declarations 或通过 [=43 共享的隐式=]-文件.

一旦foomodule的函数foo被标记为api:

cpdef api double foo(double x):
    return x*x

cpdef-functionfoo的地址可以在foomodule.__pyx_capi__字典中找到:

import foomodule
foomodule.__pyx_capi
# {'foo': <capsule object "double (double)" at 0x7fe0a46f0360>}

PyCapsule in Python. One possibility is to use ctypes.pythonapi 中提取地址非常困难,另一个(可能更容易)是利用 Cython 访问 Python 的 C-API:

%%cython
from cpython.pycapsule cimport  PyCapsule_GetPointer, PyCapsule_GetName
def address_from_capsule(object capsule):
    name = PyCapsule_GetName(capsule)
    return <unsigned long long int> PyCapsule_GetPointer(capsule, name)

可用作:

addr = address_from_capsule(foomodule.__pyx_capi__['foo'])

但是,numba 提供了类似的开箱即用功能 - get_cython_function_address :

from numba.extending import get_cython_function_address
addr = get_cython_function_address("foomodule", "foo")

一旦我们得到了c-function的地址,我们就可以构造一个ctypes函数:

import ctypes
foo_functype = ctypes.CFUNCTYPE(ctypes.c_double, ctypes.c_double)
foo_for_numba = foo_functype(addr)

这个函数可以被利用,例如nopython-numba:

from numba import njit
@njit
def use_foo(x):
    return foo_for_numba(x)

现在:

use_foo(5)
# 25.0

产生了预期的结果。