ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Numba @cfunc 完全指南:使用 LLVM 编译的 C 回调与 C/C++ 原生库互操作

Numba @cfunc 完全指南:使用 LLVM 编译的 C 回调与 C/C++ 原生库互操作 编译器高性能计算【免费下载链接】numbaNumPy aware dynamic Python compiler using LLVM项目地址https://gitcode.com/gh_mirrors/nu/numba点击查看免费下载Numba 的cfunc装饰器允许你将一段 Python 函数编译成符合 C ABI应用二进制接口的原生回调其函数指针可直接传递给 C/C 编写的第三方库例如数值积分库、事件驱动框架或回调式 API。本文以 docs/source/user/cfunc.rst 为核心结合仓库源码numba/core/decorators.py、numba/core/ccallback.py、numba/np/numpy_support.py 等深入讲解cfunc的使用方法、指针与数组内存处理、C 结构体互操作、签名规范、编译选项以及反向场景——在jit函数中调用 C 代码帮助你打通 Python 与原生世界的双向通道。cfunc是什么为什么需要 C 回调许多原生库例如用 C 或 C 编写在内部驱动计算流程需要宿主程序向它回灌业务逻辑。典型场景包括scipy.integrate.quad等积分器需要反复调用被积函数排序/搜索库接受比较器回调GUI 或事件驱动库需要事件处理函数数值求解器需要导数、代价函数等回调。如果每次都回调到 Python 解释器性能开销会非常可观。cfunc正是为解决这个问题而生它把 Python 函数编译成机器码并以你指定的签名对外暴露使原生库可以直接通过函数指针调用完全绕过 Python 解释器。cfunc与jit用法类似但有一个关键区别必须显式传入一个且仅一个签名。该签名决定了 C 回调对外可见的函数原型例如float64(float64, float64)。源码中 numba/core/ccallback.py#L41-L49 也证实构造CFunc对象时会拆分签名且必须给出明确的返回类型否则直接抛出TypeError(C callback needs an explicit return type)。基本用法把一个函数编译成 C 回调最简单的形式如下from numba import cfunc cfunc(float64(float64, float64)) def add(x, y): return x y编译完成后返回的CFunc对象暴露两类关键资源add.address编译后 C 回调的函数指针地址整数可直接传给任何外部 C/C 库add.ctypes一个ctypes.CFUNCTYPE回调对象它既可以被 C 代码调用也可以直接从 Python 调用方便本地验证编译结果cfunc(float64(float64, float64)) def add(x, y): return x y print(add.ctypes(4.0, 5.0)) # 输出 9.0从实现看ctypes属性由 numba/core/ccallback.py#L111-L119 中的cached_property生成它会根据签名把 Numba 类型逐一转换为对应的 ctypes 类型to_ctypes构造出ctypes.CFUNCTYPE(restype, *argtypes)并绑定函数地址。CFunc还额外提供cffi属性返回 cffi 函数指针见 numba/core/ccallback.py#L100-L109以及inspect_llvm()方法用于查看生成的 LLVM IR。实战示例加速scipy.integrate.quad原文档给出了一个经典案例把被积函数编译成 C 回调交给scipy.integrate.quad使用。quad既接受普通 Python 回调也接受包装在 ctypes 回调对象中的 C 回调。 import numpy as np from numba import cfunc def integrand(t): return np.exp(-t) / t**2 ...: nb_integrand cfunc(float64(float64))(integrand)注意这里展示了cfunc的非装饰器调用形式cfunc(sig)(func)适合对已有函数动态包装。接下来把nb_integrand.ctypes传给quad与纯 Python 函数结果对比 import scipy.integrate as si def do_integrate(func): Integrate the given function from 1.0 to inf. return si.quad(func, 1, np.inf) ...: do_integrate(integrand) (0.14849550677592208, 3.8736750296130505e-10) do_integrate(nb_integrand.ctypes) (0.14849550677592208, 3.8736750296130505e-10)两次积分结果完全一致误差估计也相同说明 C 回调在数值上等价。性能收益来自每次求值不再进入 Python 解释器 %timeit do_integrate(integrand) 1000 loops, best of 3: 242 µs per loop %timeit do_integrate(nb_integrand.ctypes) 100000 loops, best of 3: 13.5 µs per loop本例中整体积分加速约 18 倍。原文档中的基准数据是在当时的运行环境下测得实际加速比会随硬件、Python 版本和积分函数复杂程度变化但方向一致回调越频繁、被积函数越简单收益越明显。处理指针与数组内存carray/farrayC 语言没有类似 NumPy 数组的高层抽象因此涉及数组数据的 C 回调签名只能传递底层指针 尺寸参数。而回调体内的 Python 代码依然可以享受 NumPy 数组的表达力——桥梁就是numba.carray。以下示例中C 回调的签名为void(double *input, double *output, int m, int n)即输入、输出各一个二维数组的指针外加行列数from numba import cfunc, types, carray c_sig types.void(types.CPointer(types.double), types.CPointer(types.double), types.intc, types.intc) cfunc(c_sig) def my_callback(in_, out, m, n): in_array carray(in_, (m, n)) out_array carray(out, (m, n)) for i in range(m): for j in range(n): out_array[i, j] 2 * in_array[i, j]这里有两个要点签名构建可以用字符串签名也可以用 Numba 类型对象构建——types.CPointer(types.double)表示double*types.intc表示 C 的int32 位。字符串与类型对象两种形式等价复杂签名建议用类型对象形式可读性更好。carray语义carray(ptr, shape)以给定形状在指针指向的数据上建立数组视图不拷贝数据数据按C 顺序行优先布局。若数据是Fortran 顺序列优先则应使用numba.farray。从源码看carray的实现在 numba/np/numpy_support.py#L590-L625它接受 ctypes 指针或实现了_as_parameter_协议的对象通过_as_parameter_提取裸地址将指针类型映射为types.CPointer并推断元素 dtype最后用np.frombuffer加reshape构造视图farraynumba/np/numpy_support.py#L628-L636则通过反转 shape 再转置实现 Fortran 顺序视图。carray/farray还支持可选dtype参数用于显式指定元素类型对void*参数必须显式给出 dtype否则抛TypeError。处理 C 结构体当应用需要传递大量状态时把数据打包进 C 结构体是很自然的做法。Numba 提供两条路径把 C 结构体映射为可用的 Numba 类型。方式一借助 CFFI 自动映射通过numba.core.typing.cffi_utils.map_type可以把 cffi 类型转换为 Numba 的Record类型from numba.core.typing import cffi_utils nbtype cffi_utils.map_type(cffi_type, use_record_dtypeTrue)注意use_record_dtypeTrue是必须的。否则指向结构体的指针会被映射成void*见 numba/core/typing/cffi_utils.py#L119-L124其中void指针返回types.voidptr。注意自 v0.49 起numba.cffi_support模块已被淘汰统一使用numba.core.typing.cffi_utils。完整示例——先定义 C 结构体和回调原型再用map_type推导出 Numba 签名from cffi import FFI src /* Define the C struct */ typedef struct my_struct { int i1; float f2; double d3; float af4[7]; // arrays are supported } my_struct; /* Define a callback function */ typedef double (*my_func)(my_struct*, size_t); ffi FFI() ffi.cdef(src) # Get the function signature from *my_func* sig cffi_utils.map_type(ffi.typeof(my_func), use_record_dtypeTrue) # Make the cfunc from numba import cfunc, carray cfunc(sig) def foo(ptr, n): base carray(ptr, n) # view pointer as an array of my_struct tmp 0 for i in range(n): tmp base[i].i1 * base[i].f2 / base[i].d3 tmp base[i].af4.sum() # nested arrays are like normal NumPy arrays return tmp关键机制在 numba/core/typing/cffi_utils.py#L97-L135 的map_type它按 cffi 类型的kind分派——function类型递归映射参数与返回类型并生成签名pointer类型映射为types.CPointerarray类型映射为NestedArraystruct类型在use_record_dtypeTrue时调用map_struct_to_record_dtypenumba/core/typing/cffi_utils.py#L138-L168逐字段构造 NumPy Record dtype记录名称、格式、偏移量、对齐信息。不受支持的情况union 类型直接抛TypeError带位域bitshift/bitsize或非常规 flags 的字段会抛ValueError变参函数ellipsis同样不支持。回调体内base[i].i1这样的字段访问、af4.sum()这样的嵌套数组运算都遵循普通 NumPy 数组的语义——这正是Recorddtype 带来的便利。方式二手工构造types.Record.make_c_struct不依赖 CFFI 时可以用types.Record.make_c_struct手工按 C 结构体内存布局构造 Record 类型my_struct types.Record.make_c_struct([ # 提供 (name:str, type:Type) 二元组序列 (i1, types.int32), (f2, types.float32), (d3, types.float64), (af4, types.NestedArray(dtypetypes.float32, shape(7,))), ])这里NestedArray对应 C 中的内嵌定长数组float af4[7]。ABI 限制结构体应以指针形式传递即参数类型写types.CPointer(my_struct)在cfunc函数体内再用carray把my_struct*视作结构体数组访问。原文档提示完整示例见examples/notebooks/Accessing C Struct Data.ipynb该 notebook 不在当前仓库树内读者可参考上述两种方式的代码片段直接落地。签名规范什么样的类型适合 C 回调cfunc的显式签名可以使用任意 Numba 类型但只有一部分类型对 C 回调有意义。原文档给出的实践建议是标量类型如int8、float64、intc等基础 C 标量指向标量的指针例如types.CPointer(types.int8)指向Record类型的指针用于结构体回调。结合 numba/core/ccallback.py#L17-L30 的实现可知C 回调不支持 object 模式编译器会把flags.enable_pyobject置为False若显式设置force_pyobject会直接抛NotImplementedError(object mode not allowed in C callbacks)源码注释说明原因是没有可靠的方式在回调中获取环境。因此签名必须限定在可原生编译的类型范围内。编译选项nopython与cachecfunc接受若干仅限关键字keyword-only的参数nopython和cache语义与jit装饰器中的同名参数一致。nopython强制 nopython 模式编译。如前所述object 模式在 C 回调中不受支持因此实际使用中通常应保持或默认启用nopython 语义。cache启用磁盘缓存避免重复编译。从 numba/core/decorators.py#L256-L281 的实现看cacheTrue时会调用res.enable_caching()为回调对象挂上FunctionCache编译时优先从缓存加载_cache.load_overload命中则计入cache_hits可通过CFunc.cache_hits属性观察未命中才走完整编译流程并回写缓存numba/core/ccallback.py#L59-L76。用法示例cfunc(float64(float64, float64), nopythonTrue, cacheTrue) def add(a, b): return a b另外cfunc还接受locals局部变量类型映射与pipeline_class自定义编译管线高级用法默认compiler.Compiler。反向互操作在 Numbajit中调用 C 代码cfunc解决的是把 Python 逻辑暴露给 C反过来Numba 也支持在jit函数中直接调用 C 代码。原文档给出了两个方案。方案一通过 LLVM 加载共享库先用 C 编写一个加法函数并编译为共享库#include stdint.h int64_t sum(int64_t a, int64_t b){ return a b; }编译命令gcc lib.c -fPIC -shared -o shared_library.so原文档注明该示例在 Linux 上验证Unix-like 系统通常可用。然后在 Python 中把库加载进 LLVM并用types.ExternalFunction声明外部函数签名from numba import njit from numba.core import types, typing from llvmlite import binding import os # load the library into LLVM path os.path.abspath(./shared_library.so) binding.load_library_permanently(path) # Adds typing information c_func_name sum return_type types.int64 argty types.int64 c_sig typing.signature(return_type, argty, argty) c_func types.ExternalFunction(c_func_name, c_sig) njit def example(x, y): return c_func(x, y) print(example(3, 4)) # 7binding.load_library_permanently把共享库符号加载进 LLVM 进程types.ExternalFunction让 Numba 类型系统认识sum的原型之后就能在njit函数体中像调用普通函数一样调用它。方案二通过 ctypes使用ctypes的好处是与 JIT 装饰器无关同样的回调对象既可被njit函数使用也可被纯 Python 调用from numba import njit import ctypes DSO ctypes.CDLL(./shared_library.so) # Add typing information c_func DSO.sum c_func.restype ctypes.c_int c_func.argtypes [ctypes.c_int, ctypes.c_int] njit def example(x, y): return c_func(x, y) print(example(3, 4)) # 7 print(example.py_func(3, 4)) # 7example.py_func是未编译的原始 Python 函数这里两行输出均为7说明同一个 ctypes 回调对象在 Numba 编译代码和纯 Python 中调用结果一致。小结cfunc的核心要点要点说明必传签名与jit不同cfunc必须显式指定唯一签名且返回类型不能省略导出方式address裸函数指针供 C/C 库使用、ctypesCFUNCTYPE对象Python 侧可验证、cfficffi 函数指针数组互操作用carrayC 顺序/farrayFortran 顺序在指针上建立 NumPy 视图void*参数必须显式给 dtype结构体互操作CFFImap_type(..., use_record_dtypeTrue)自动映射或types.Record.make_c_struct手工构造结构体一律以指针传递编译约束不支持 object 模式签名建议使用标量、标量指针、Record 指针编译选项关键字参数nopython、cache可配合locals、pipeline_class高级选项反向调用jit中可通过ExternalFunctionLLVM 加载共享库或 ctypes 调用 C 代码cfunc是 Numba 生态中连接 Python 与 C/C 原生库的关键桥梁。它让 Python 在保留 NumPy 表达力的同时把热点路径上的回调彻底原生化无论你是要给积分器喂被积函数、给求解器喂回调还是要让事件框架直接驱动你的业务逻辑它都提供了低成本、高性能的落地方案。赞分享编译器高性能计算【免费下载链接】numbaNumPy aware dynamic Python compiler using LLVM项目地址https://gitcode.com/gh_mirrors/nu/numba点击查看免费下载相关推荐Numba cfunc与C互操作从C代码直接调用Python编译函数Numba cfunc与C互操作从C代码直接调用Python编译函数 Numba 的 cfunc 装饰器能将纯 Python 函数编译为 标准 C编译器高性能计算Kotlin/Native与C/C互操作原生库集成完全指南Kotlin/Native与C/C互操作原生库集成完全指南 引言为什么需要原生互操作 在现代软件开发中我们经常需要利用现有的C/C库来获得高性能编译器语言运行时编程语言Codon 与 C/C 互操作指南将 Codon 函数编译为共享库并从 C/C 调用Codon 与 C/C 互操作指南将 Codon 函数编译为共享库并从 C/C 调用 本指南讲解如何在 Codon 中将函数编译为共享库shared编译器编程语言语言运行时创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表