Python 使用 C 扩展提升性能的思路

1次阅读

该考虑写c扩展当且仅当python逻辑已成性能瓶颈、numba不支持热点函数、或ctypes/cffi因频繁内存拷贝拖慢性能;否则优先选pybind11或cffi。

Python 使用 C 扩展提升性能的思路

什么时候该考虑写 C 扩展,而不是用 Cython 或 numba

Python 调用纯 Python 逻辑慢到瓶颈、且热点函数无法被 numba.jit 支持(比如涉及动态类型、字典嵌套、异常控制流),或者你已经在用 ctypes / cffi 但频繁内存拷贝拖垮性能——这时候才值得上 C 扩展。别为“听说 C 快”就动手,90% 的场景 pybind11cffi 更省事。

常见错误现象:ImportError: dynamic module does not define module export function,往往是因为 PyModuleDef 结构体没填对,或模块初始化函数名不匹配。

  • 使用场景:高频调用的数值计算内核、自定义序列化/解析器、绕过 GIL 的 I/O 密集型操作
  • 参数差异:PyObject * 是唯一入口参数类型,所有 Python 对象都得从它 unpack;C 层不能直接用 intFloat 类型的 Python 参数
  • 性能影响:如果 C 函数里频繁调用 PyLong_AsLongPyList_GetItem,反而比纯 Python 慢——C 扩展快的前提是“少跨边界、少查类型、少分配 Python 对象”

如何让 C 扩展不崩溃 Python 解释器

核心原则:只要 C 层抛了未捕获的 SIGSEGV、访问了已释放的 PyObject *、或在 GIL 释放后误用 Python C API,解释器就直接 abort。这不是报错,是段错误。

实操建议:

立即学习Python免费学习笔记(深入)”;

  • 所有 PyArg_ParseTuple 后必须检查返回值,失败时立即 return NULL,不能继续执行
  • Py_INCREF / Py_DECREF 管理引用计数——尤其在循环中反复赋值 item = PyList_GetItem(...) 时,item 不增加引用,不能直接存起来后续用
  • 若需释放 GIL(如长耗时计算),必须用 Py_BEGIN_ALLOW_THREADS / Py_END_ALLOW_THREADS 包裹,并确保中间不调任何 Python C API
  • 调试时加 -g -O0 编译,用 gdb python -c "import yourmod" 捕获 core dump

setup.py 编译 C 扩展最简可靠写法

别碰 distutils,用 setuptools.Extension + setup(),且显式指定 include_dirsextra_compile_argswindows 上尤其容易因 MSVC 版本不匹配导致 LNK2001

关键点:

  • Extensionsources 列表必须包含 .c 文件全路径,不能只写文件名
  • linux/macosextra_compile_args=['-fPIC'],否则 ImportError: dynamic module does not define module export function
  • 避免用 python setup.py build_ext --inplace 反复编译——.so/.pyd 文件可能被占用,改完 C 代码后先删掉 build/ 目录再重来
  • 验证是否成功:python -c "import yourmod; print(yourmod.__file__)",输出路径应指向 .so/.pyd,不是 .py

为什么你的 C 扩展跑得还没纯 Python 快

典型原因不是 C 写得差,而是边界开销吃掉了全部收益。一次 PyObject *double 的转换成本 ≈ 50 条 C 浮点指令;传 1000 个 int 进去,逐个 PyLong_AsLong,不如 Python 层用 Array.arraynumpy.ndarray 一次性传内存块。

  • 正确做法:用 PyMemoryView_FromObjectPyArray_FROM_OTF(配 numpy)接数组,直接操作 data 指针
  • 避免在 C 层构造新 PyListPyDict 返回——能用 Py_BuildValue("i", result) 就别手动生成对象
  • Windows 上注意 __declspec(dllexport) 不要漏写,否则 ImportError 报得毫无提示
  • 真正快的 C 扩展,90% 代码在做数据搬运和校验,核心计算逻辑往往就十几行

最难的从来不是写 C,是想清楚哪部分真该交给 C —— 多数人卡在把 Python 的抽象泄漏当成性能瓶颈。

text=ZqhQzanResources