news 2026/10/15 22:44:21

Python C++混合编程实战:用pybind11打造高性能扩展

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python C++混合编程实战:用pybind11打造高性能扩展

很多做算法的人都会遇到同一个坎:Python 写起来舒服,跑起来却慢;C++ 跑得快,写起来又费劲。去年我给一个图像处理 Demo 提速,把一段双重循环从 Python 挪到 C++ 里,单独这段函数耗时从 420 毫秒降到了 9 毫秒,调用层还是 Python,整个工程只改了一个文件。这次 C++ 与 Python 混合编程实战做完,最大的体会是:技术难点不在写 C++,也不在写 Python,而在怎么让两种语言在同一个进程里高效、安全地协作。

这篇文章会从方案选型、环境搭建、代码绑定、回调机制、数据搬运到打包调试,完整过一遍我自己的实操路径。不管你是想把 Python 原型里某个瓶颈换成 C++,还是想给老 C++ 库套一层 Python 壳,都可以照着下面的步骤复现。

1. 先厘清一个问题:到底谁在跑谁的代码

混合编程最容易犯的错,是一上来就动手写代码,却没想清楚运行模型。我习惯先画一句话:进程是同一个,解释器是 CPython,C++ 写的东西被编译成动态库后,以模块的形式被 Python 加载进来。执行到import mymodule的那一刻,Python 解释器调起动态库的初始化函数,把 C++ 注册的类、函数挂到 PyModule 上,后面调用就是纯粹的函数跳转。

这跟很多人设想的“两个程序之间通信”完全不同。比如我自己早年以为的是 Python 调起一个子进程去跑 C++ 可执行文件,再用文件或管道传数据。那条路在批量任务里勉强能忍,但一是每次调用都有进程开销,二是完全没法共享内存里的对象,三是异常处理会非常别扭。数据库驱动、图像库、科学计算库的实际做法,几乎全是在解释器内部直接加载动态库,让 Python 对象和 C++ 对象地址互相暴露。共享地址空间是高性能的基础,没有共享,就没有混编。

理解了这个模型之后,很多选择就顺理成章了。例如为什么封装文件用mymodule.so或mymodule.pyd,而不是普通可执行文件;为什么编译时必须有 Python 的头文件和库文件;为什么运行时不能随便换 Python 小版本,因为两种语言共享一个解释器实例,ABI 不一致时很容易崩。这些不是玄学,都是底层机制决定的。

顺带说下适用范围。我的经验里,最值得用混合编程的是这些场景:

  • 数值循环密集,Python 的for循环或列表推导撑不住的地方;
  • 已有 C++ 引擎或第三方 C++ 库,不想用重写来迁到 Python;
  • 需要直接操作内存缓冲区,比如摄像头帧、音频采样点、GPU 返回的 buffer;
  • 对延迟敏感的嵌入式调用,比如实时期的 python 回调路径。

反过来,频率极低的磁盘 IO、一次性的配置解析,就没必要放进 C++。混编得有收益才做,别为炫技把维护成本抬上去。

1.1 纯 Python 卡在哪

Python 慢通常不是算术本身慢,而是每条字节码都有解释和派发开销。拿一段双层循环做二维卷积举例,纯 Python 版本每一层循环都要查 dict、建临时对象、做类型检查、再回收垃圾。中间若涉及 numpy 标量,还会产生无数次 Python 对象和 C 标量之间的来回转换。量一大,时间全部耗在“维护 Python 对象模型”上了。

C++ 里同样的循环,编译器直接把循环展开成寄存器操作,每个元素只是从数组读出来乘一下再写回去。两者的差异一般是一到两个数量级。如果数据规模过大,还能在 C++ 侧直接开多线程或者用 SIMD,而 Python 侧只需看到同样的函数签名。

但这不意味着 C++ 全能赢。代码里只要频繁发生“Python 对象与 C++ 对象转换”,刚赚回来的性能就可能又赔回去。所以混编的核心原则是:让转换发生在函数边界上,一次进、一次出;函数内部保持纯 C++ 操作。凡是违反这个原则的封装,后期 profile 时都会看见大量时间花在PyArg_ParseTuple和PyObject*的引用计数操作里。

2. 四种互通方案,怎么选才不会掉坑

市面上常见的方案有 ctypes、Cython、原生的 CPython C API、pybind11 这几种。我一开始图省事,先用 ctypes 把 C++ 编译成 C 风格接口,结果发现类对象、重载、STL 容器都得手动写一层 extern "C" 的中间层,简直是在重复发明车轮子。后来切到 pybind11,同样是写封装,代码量直接减了五分之四。

先看一张我常用的选型表:

方案使用体验绑定开发量运行开销适合情况
ctypes直接用 Python 调用 C/C++ 动态库中,需手写 extern C每次调用有参数转换成本快速验证、只调少量函数
Cython用类似 Python 语法写 C 扩展中,需要学 Cython 语法低,接近手写 C对代码做增量加速
原生 CPython C API手工管理 PyObject 引用计数高,极易泄漏或崩溃极低追求极限控内存,但不想用第三方库
pybind11头文件库,声明式绑定低,纯 C++ 写绑定代码低,参数转换高度优化绝大多数项目首选

2.1 ctypes 适合的边界

如果你只是想调用别人编译好的 C 函数,比如某个 DLL 里有现成的int add(int a, int b),ctypes 确实是最快的路径。不用写 C++ 编译代码,Python 里声明参数类型和返回类型即可。但它有个天然缺陷:对象封装极难。C++ 类把数据和操作绑在一起,对象的构造、复制、析构都要由你手动安排,ctypes没有隐藏 object 成员的机制,几乎没法优雅地映射。

我在一个模拟项目里用 ctypes 封装过一个 C++ 日志库。为了能把 C++ 类实例用指针形式留在 Python 进程里,我不得不用c_void_p保存this指针,再为每个成员函数单独写包装。封装层比业务代码还长。此类项目一次尚可,长期维护非常痛苦。

2.2 为什么我默认先选 pybind11

pybind11 本质上是一套 C++ 模板元编程库,基于 C++11 就能用,不需要额外生成代码,不需要独立编译步骤。它的绑定声明长这样:PYBIND11_MODULE(mymodule, m) { m.def("add", &add); }。模板会在编译期完成参数类型到 Python type 的映射,运行时的转换路径很短。

它支持的 C++ 特性对我非常关键:类继承、重载、运算符、STL 容器、智能指针、lambda、enum。绑定一个类只需要py::class_<MyClass>(m, "MyClass"),成员函数用.def("method", &MyClass::method)挂上去,比手写PyTypeObject省掉大量样板代码。而且它内部自动处理引用计数、异常转换、GIL 的获取释放,这些细节要自己写很容易犯错。

必须说明的是,pybind11 对编译器有一定要求。Windows 上我建议用 MSVC 2019 或 2022,Linux 上用 GCC 5.4 以上,macOS 上用 Clang 10 以上。老编译器也不是不能用,C++11/14 模式下几乎能编过,但有些模板提示信息会难看得多,排错成本显著上升。

3. 从 0 到 1:编译一个能被 import 的 C++ 加速模块

我把流程分成三步,每步都可以单独验证,哪里出错一眼就能看到。

3.1 工程结构先摆好

我习惯这样组织目录:

mymodule/ ├── CMakeLists.txt ├── src/ │ ├── fast_ops.cpp │ └── fast_ops.hpp ├── tests/ │ └── test_import.py └── setup.py # 打包时用

CMake 是更稳的选择,因为 pybind11 官方提供pybind11_add_module函数,会自动找到 Python 开发头文件,并把编译选项设置好。setup.py 那套传统方式用Extension模块也能跑,但跨平台时经常要手动指定编译参数,比如 Windows 的/EHsc,Linux 的-fvisibility=hidden,很容易漏。所以我从 CMake 起步。

3.2 CMakeLists.txt 最简版本

提示:下面的构建方式以 pybind11 的 CMake 集成模式为例,这是我在 Windows 和 Linux 上都验证过的路径。

cmake_minimum_required(VERSION 3.15) project(mymodule CXX) find_package(Python COMPONENTS Interpreter Development REQUIRED) find_package(pybind11 CONFIG REQUIRED) pybind11_add_module(mymodule src/fast_ops.cpp) target_compile_features(mymodule PRIVATE cxx_std_17) if(MSVC) target_compile_options(mymodule PRIVATE /W3 /EHsc /bigobj) else() target_compile_options(mymodule PRIVATE -O3 -Wall -fvisibility=hidden) endif()

如果你没安装 pybind11 到系统目录,可以用 FetchContent 直接从源码拉取,但那样每次构建都要联网。我本地用了一个 pybind11 的头文件目录,直接在 CMake 变量里指定路径更省事:

set(pybind11_DIR "path/to/pybind11/share/cmake/pybind11")

这种做法对 CI 也很友好,构建机只要配好工具链,不需要额外的 pip 包。

3.3 最简的加速函数:先跑通再谈复杂

拿一个矩形灰度图求局部均值的操作来试。Python 侧双循环太慢,我把它换成 C++ 函数,只接收 numpy 数组和半径参数,返回同样尺寸的数组。

简化版fast_ops.cpp:

#include <pybind11/pybind11.h> #include <pybind11/numpy.h> namespace py = pybind11; py::array_t<uint8_t> box_filter(py::array_t<uint8_t> input, int radius) { auto buf = input.request(); int h = static_cast<int>(buf.shape[0]); int w = static_cast<int>(buf.shape[1]); py::array_t<uint8_t> output({h, w}); auto outbuf = output.request(); uint8_t* src = static_cast<uint8_t*>(buf.ptr); uint8_t* dst = static_cast<uint8_t*>(outbuf.ptr); for (int y = 0; y < h; ++y) { for (int x = 0; x < w; ++x) { int sum = 0, count = 0; for (int dy = -radius; dy <= radius; ++dy) { for (int dx = -radius; dx <= radius; ++dx) { int ny = y + dy; int nx = x + dx; if (ny >= 0 && ny < h && nx >= 0 && nx < w) { sum += src[ny * w + nx]; ++count; } } } dst[y * w + x] = static_cast<uint8_t>(sum / count); } } return output; } PYBIND11_MODULE(mymodule, m) { m.def("box_filter", &box_filter, "二维均值滤波加速", py::arg("input"), py::arg("radius") = 1); }

这个函数里最值得注意的一点:我没有在循环里做任何 Python 对象操作。input.request()只调用一次,把数组的指针、维度、步长一次取出来,后面全是裸指针上的 C++ 运算。输出数组也是 C++ 侧分配的,然后整体变成 Python 对象返回。这样调用边界一览无余。

3.4 编译命令与首次 import 验证

构建:

cmake -S . -B build -DCMAKE_BUILD_TYPE=Release cmake --build build --config Release

生成物会在build/Release/mymodule.pyd或build/mymodule.so。验证环节,我习惯先写个最简单的脚本:

import mymodule import numpy as np img = np.arange(10000, dtype=np.uint8).reshape(100, 100) out = mymodule.box_filter(img, 1) print(out.shape, out.dtype) print(out[:3, :3])

如果能正常打印,说明动态库加载、C++ 函数调用、numpy 数组传递全部走通。这一步我会在写更复杂的类封装前反复确认,因为环境问题能把你后面所有的排错方向带偏。

4. 把 C++ 类真正当成 Python 对象用:类绑定与内存生命周期

函数级封装加速足够应付一批场景,但真实项目里更多时候是想把 C++ 里的“引擎”或“服务”整体暴露出来。比如我有一个模拟的电池状态估计算法库,内部维护了模型参数矩阵和历史观测序列,如果每次调用都把内部状态导出成 numpy 数组再传回 Python,来回开销很大。正确做法是把那个 C++ 类对象绑定成一个 Python 对象,让 Python 持有它的句柄,内部状态待在 C++ 侧,需要时再选择性暴露。

4.1 绑定构造、方法、属性

下面用一个更简单的 C++ 类演示。这个类维护一个环形缓冲区,满员时自动覆盖最旧数据。

class RingBuffer { public: explicit RingBuffer(int capacity) : cap_(capacity), data_(capacity, 0.0), head_(0), size_(0) {} void push(double v) { data_[head_] = v; head_ = (head_ + 1) % cap_; if (size_ < cap_) size_++; } double mean() const { if (size_ == 0) return 0.0; double sum = 0.0; for (int i = 0; i < size_; ++i) sum += data_[i]; return sum / size_; } int capacity() const { return cap_; } int count() const { return size_; } private: std::vector<double> data_; int cap_; int head_; int size_; };

绑定:

#include <pybind11/stl.h> namespace py = pybind11; PYBIND11_MODULE(mymodule, m) { py::class_<RingBuffer>(m, "RingBuffer") .def(py::init<int>()) .def("push", &RingBuffer::push, py::arg("value")) .def("mean", &RingBuffer::mean) .def_property_readonly("capacity", &RingBuffer::capacity) .def_property_readonly("count", &RingBuffer::count); }

然后 Python 侧就能这样用:

import mymodule buf = mymodule.RingBuffer(100) for i in range(1000): buf.push(i * 0.5) print(buf.capacity, buf.count, buf.mean())

令人舒适的地方在于:你不会看到buf是个“假 Python 对象”的痕迹,repr、垃圾回收、copy.deepcopy都表现正常。模板替你生成了PyObject的操作代码。

4.2 生命周期和所有权:析构与悬挂指针的高发区

绑定类最容易翻车的是所有权模型。pybind11 里py::class_默认对工厂函数返回的裸指针使用“不接管所有权”,对py::init构造的对象使用“接管所有权”。如果混用,会出现一个对象被 Python 垃圾回收析构了,但 C++ 侧还在用得着它的情况,或者反过来,Python 侧从不析构,C++ 内存一直不释放。

举一个我的真实踩坑例子:我封装过一个返回单例的工厂函数。

static MyEngine* get_engine() { return global_engine_; } m.def("get_engine", &get_engine, py::return_value_policy::reference);

在 Python 侧持续调用它,然后删除引用,程序退出时出现了重复析构崩溃。原因是默认return_value_policy::automatic会在没有其他持有者时尝试接管并销毁返回值,而那个指针指向全局单例,根本不该被销毁。改成py::return_value_policy::reference后,pybind11 就只返回指针,绝不释放。

所有权 policy 这块,我根据自己的经验做了个小总结:

返回方式建议 policy
py::init 构造的对象默认接管即可
返回类内部持久的引用reference
返回新分配、想交给 Python 管理的裸指针take_ownership
返回 shared_ptrautomatic,但类型要声明 shared_ptr 版本
返回 static/全局变量reference 或 reference_internal

每次绑定方法前,先在注释里想清楚“这个返回值的归属是谁”,再选 policy。别偷懒全部默认,否则后面 profile 一压测就崩。

4.3 STL 容器与 Python 容器互转

std::vector<int>、std::map<std::string, double>这类容器和 Python list/dict 的互换,是 pybind11 最贴心的部分。只要#include <pybind11/stl.h>,绑定函数签名里的容器会自动转换。

但代价是每次转换都会发生一次数据拷贝。Python list 转成std::vector,得把每个 PyObject 转为 int;返回std::vector,又得为每个元素建新的 PyObject。这在数据量小的时候完全感知不到,但如果是 10 万元素以上的容器,拷贝耗时就很明显。

因此我有个习惯:跨语言边界传递的大块连续数据,一律用 numpy 数组或py::buffer走内存共享;小配置数据,如参数列表、字典,才用 STL 容器让它图方便。前者面向性能,后者面向开发效率。这两者的分界线一定要心里有数。

5. 双向回调:C++ 调用 Python 函数时,GIL 是绕不开的坎

混编不止是 Python 调 C++,很多场景反过来要求在 C++ 里回调 Python 函数。比如我用 C++ 写了个异步队列消费者,每次消费完一个消息,要把结果交回 Python 层做后续处理。坑就从这里开始。

5.1 单线程里回调似乎很顺

最单纯的场景,Python 调用 C++ 函数,C++ 函数内部直接构造一个py::function参数回调,比如:

void apply_async(py::function cb, int value) { int result = value * 2; cb(result); }

第一次跑完全没毛病,回调能正常执行。因为这一整条调用链都在持有 GIL 的那个线程里,Python 对象操作是被允许的。只要不存在第二个线程抢解释器锁,这就是安全的。

5.2 换成 C++ 线程后开始随机崩溃

一旦把回调扔进 C++ 工作线程,情况完全不同。下面的代码就有问题:

void apply_async_threaded(py::function cb, int value) { std::thread([cb, value]() { int result = value * 3; cb(result); // 危险:另一个线程没有 GIL }).detach(); }

Python 解释器规定,任何线程想操作 Python 对象,必须先拿到全局解释器锁 GIL。原生线程直接调用py::function,等于在没有 GIL 的情况下访问 Python 运行时。表现通常是三种:解释器直接 Segfault;回调莫名其妙延迟;或者在极端调度下死锁。原因是 CPython 内部对象全部依赖 GIL 做并发保护,裸线程进来就相当于无锁访问共享内存。

5.3 正确姿势:抢占 GIL、释放时机和防死锁

pybind11 提供了py::gil_scoped_acquire来获取 GIL,还有py::gil_scoped_release在不需要 Python 对象时主动释放。一个安全的线程池回调函数应该这样写:

#include <pybind11/functional.h> void apply_async_threaded(py::function cb, int value) { std::thread([cb, value]() { // 进入 Python 世界之前,显式获取 GIL py::gil_scoped_acquire gil; cb(value * 3); }).detach(); }

gil_scoped_acquire的构造会等待当前线程拿到 GIL,析构时自动释放。我把这句话记成:给 C++ 线程戴上一顶“临时 Python 帽子”,操作完就摘掉。

还要防一个死锁场景:假设 C++ 主线程持有 GIL,同时又 join 一个内部的 C++ 子线程,子线程在等 GIL,主线程在等子线程结束,就会僵死。解决方案是主线程在 join 之前调用py::gil_scoped_release释放 GIL,让子线程先完成任务,再重新获取。

void dangerous_wait(std::thread&& t) { t.join(); // 主线程还拿着 GIL,子线程拿不到 -> 死锁 } void safe_wait(std::thread&& t) { { py::gil_scoped_release release; t.join(); } }

这一节是混编里最容易“测试时没问题、上线就随机奔溃”的部分。没有任何 profile 工具能比一个明确的 GIL 管理方案更有效。我在所有回调链路上都会加注释标明“本线程是否持有 GIL”,这是血的教训换来的习惯。

6. 大数据搬运:numpy 交互的两种正确姿势

如果混编只传标量和几百字节的配置,性能坑不明显。真正让混编加速失效的是大数据搬运方式没选对。同样一个函数,数据复制与零拷贝的耗时能差出 4 到 10 倍,尤其在高分辨率图像或帧序列上。

6.1 拷贝路线:适用性广但次数受限于体量

pybind11 里用py::array_t<T>接收 numpy 数组时,常规做法是直接request()拿 buffer_info。如果函数签名里直接接收py::array_t,并且你只在函数里读一遍,那任务完成后数组仍然由 Python 管理,不需要拷贝。

但有些业务场景,C++ 侧要把数据存下来长期使用,而 Python 侧也会在后续修改同一块数组,此时如果不拷贝,就会出现数据竞争或逻辑 bug。我的方案是在存盘节点上主动做拷贝:

py::array_t<double> store_deepcopy(py::array_t<double> arr) { auto input = arr.request(); std::vector<double> storage(input.ptr, input.ptr + input.size); // 之后统一从 storage 构造新数组 py::array_t<double> out({input.shape[0], input.shape[1]}); std::copy(storage.begin(), storage.end(), static_cast<double*>(out.request().ptr)); return out; }

这个例子里存储向量就是深拷贝,后续 C++ 改动不会染指 Python 原数组。单次拷贝成本跟数组体量线性相关,但只在需要持久化时发生,可控。

6.2 零拷贝路线:内存共享与步长处理

高频路径上,我更推荐用py::buffer_info的ptr直接在原内存上操作,连 request 都只调用一次。上面均值滤波例子已经体现了这个思路。关键是两点:

  • buf.shape和buf.strides必须解析出来,不能假设数组一定是 C 连续内存;
  • 拿到ptr后要自己算偏移,data + y * strides[0] + x * strides[1]。

如果数组经过转置或切片,strides就不再是简单的[w, 1]。我把通用访问封装成一个小的二维助手类,读索引时统一用strides计算,这样切片进来的数组也能零拷贝处理。

6.3 实测数据对比

我在一个模拟的“十万点云重投影”项目里做过一次对比。函数行为完全相同,只改了内存策略:

方式单次调用耗时数据体量
纯 Python 全量数组操作与标量循环860 ms100k 点 × 3 维
C++ 深拷贝后计算45 ms转换 2.4 MB 数据
C++ 零拷贝共享内存计算12 ms仅从原数组读取

可以看到,深拷贝路线虽然已经把 Python 循环消除了,但 33ms 的拷贝成本在总量里占比不小。零拷贝这条路径才让加速效果最大化。

我的结论很简单:能零拷贝就零拷贝;非拷不可的地方,单独封装成显式函数,别藏在算法路径深处。这样性能可预期,内存行为也可预期。

7. 发布、调试与压测的真实经验

写模块不是终点,能在一个月的维护周期里不崩、能被同事在另一台机器上一键安装才是终点。这里集中讲我踩过的发布与调试坑。

7.1 wheel 打包:跨平台比想象中麻烦

开发机里import mymodule成功,不代表换台机器也能成功。最大问题是系统依赖和 Python 版本 ABI。我自己被打脸过:用 Python 3.10 构建的.so放进 Python 3.11 环境,一 import 就报undefined symbol: _Py_...。那是因为 CPython 小版本之间运行时结构有差异,扩展模块必须针对目标版本重新编译。

解决方式是用打包工具配合平台标签。Linux 上多线程 CI 构建 manylinux wheel,Windows 上分别对 x86 和 x64 构建.pyd,macOS 上构建 universal wheel。项目规模不大时,我可以接受只在同一个开发环境里构建和消费,但只要发给别人,就必须走标准的特定构建流程。

我的推荐打包组合是scikit-build-core配合pybind11的 CMake 支持。它读取 CMakeLists.txt,生成 wheel,和 pip/conda 兼容性都很好。

7.2 崩溃排查:Segfault 的分析脚本

混编一崩溃,最先排查的一定是内存问题,而不是逻辑问题。我有一套固定流程:

  1. 在 Python 脚本里先import faulthandler; faulthandler.enable(),能让 segfault 时打印调用栈;
  2. 如果栈不足够定位,再用 gdb 跑 Python 进程,gdb --args python test_script.py,崩溃处bt full看 C++ 侧栈帧;
  3. 确认崩溃是否由 GIL 或所有权 policy 导致,用最小复现脚本二分缩小;
  4. PYTHONMALLOC=malloc环境变量能绕过 Python 的小对象分配器,定位是解释器堆腐败还是自己代码堆越界。

有一次我遇到莫名“听不出异常但退出时崩”,最后定位到同一个对象被py::class_绑定后又用return_value_policy::automatic返回到 Python,导致析构被调用两次。这类问题很难靠阅读代码发现,压测一段时间后才随机出现。

7.3 构建类型和调试信息的坑

Release 模块的-DNDEBUG会让assert全部失效,逻辑异常只能在加回打印后才暴露。我通常在开发阶段构建Debug或RelWithDebInfo版本,Python 侧照样 import,崩溃时才能保留__FILE__、__LINE__信息。到了发版阶段才换成 Release,并做一轮专门的性能回归。

有一点经常被忽略:给调试版加-fsanitize=address在 Linux 上能自动拦截越界和 use-after-free,Windows 上用 MSVC 的/fsanitize=address也能达到类似效果。ASan 输出直接告诉我“哪一行 ptr read 越界”,比 gdb 里翻汇编快太多。缺点是非常吃内存和 CPU,只适合压测小样例。

7.4 一次真实的优化排错过程

最后补一条完整链路供参考。我给某跨平台系统的数据预处理模块做混编加速,编译后测了正确性都通过,但压测黑盒时一跑满 16 个并发线程就会崩一个子进程。我按上述排查顺序走了一遍:

  • 开启 faulthandler,得到栈帧指向某个 C++ 回调函数;
  • gdb 看完整栈,发现是PyObject_Call时操作了空指针;
  • 回溯代码,是该回调函数在线程池里直接调用了py::function,完全没拿 GIL;
  • 修复成py::gil_scoped_acquire后,连续压测 48 小时没有再崩。

整个过程不到半天。如果是靠盲试,可能耗一周。调试工具链的价值就在这种时候体现。

最后的提醒:先压测再优化封装

如果让我给新手一个最实用的建议:先把 Python 原生版本的算法写稳、写对,用 profile 找到最耗时的那个函数,再把它挪进 C++。不要一开始就设计极其庞大的绑定体系。混编的核心收益来自瓶颈点上的数量级提升,而不是来自封装技术本身的花哨。同样一段逻辑,如果 C++ 侧也就比 Python 快 20%,那说明瓶颈不在执行模型上,可能在 IO 或算法复杂度,换语言解决不了。

我在拿到一个新项目时,会先画一条数据流路径,标出每个节点的数据形态和是否需要跨语言转换。转换点越少,代码越容易维护,性能也越稳定。等你完成第一个模块、尝到那种“Python 的灵活 + C++ 的性能”组合的甜头后,就会理解为什么这种方案会成为很多现代底层库的标配。剩下的,就是按本文这套流程,自己动手把第一个模块编译出来。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 22:43:19

数据科创下半场:从技术狂欢到产业扎根的突围指南

大概在两年多前&#xff0c;我参加某场技术峰会时&#xff0c;台上的嘉宾演示了一个“AI自动生成营销文案”的Demo&#xff0c;效果惊艳&#xff0c;掌声雷动。然后主持人问了一个问题&#xff1a;“这个系统在你们业务里跑起来了吗&#xff1f;”嘉宾顿了一下&#xff0c;说&a…

作者头像 李华
网站建设 2026/10/15 22:41:59

LeetCode 3013 详解:单调栈+线段树优化划分型DP

今天刷每日一题刷到了 3013&#xff0c;题号带个 II 就说明事情不简单。这道题把“划分型 DP”和“单调栈 懒标记线段树”焊死在了一起&#xff0c;而且线段树里维护的还不是普通区间最值&#xff0c;是一组会随着右端点移动而整体变化的候选值。如果你对这类结构不熟&#xf…

作者头像 李华
网站建设 2026/10/15 22:40:43

H5原生调用摄像头全攻略:从getUserMedia到移动端适配

先说一个我自己的真实经历。之前做移动端H5项目&#xff0c;页面需要直接用浏览器能力打开摄像头做人像采集。第一版Demo在电脑Chrome上跑&#xff0c;结果白屏&#xff0c;控制台日志打出一个NotAllowedError。我第一反应是权限被浏览器拦了&#xff0c;排查一圈下来才发现&am…

作者头像 李华
网站建设 2026/10/15 22:38:29

用C++与Qt构建跨平台云笔记:SQLite存储与同步协议实战

简介&#xff1a;一份基于C跨平台的云笔记系统设计论文PDF资源&#xff0c;面向需要完成C/Qt课程设计、毕业设计或对跨平台桌面应用感兴趣的开发者。该方案聚焦跨平台云笔记协作难题&#xff0c;采用C/S架构&#xff0c;以C为开发语言、TCP为通信协议、Qt为开发框架&#xff0c…

作者头像 李华
网站建设 2026/10/15 22:37:36

AutoDL+Strata部署Qwen3.8 Flash Next全流程实战

1. 部署前的准备工作与整体思路1.1 为什么选AutoDL Strata这套组合先说说我的选择逻辑。之前部署对话模型&#xff0c;我一直用常规推理框架&#xff0c;但这次拿到Qwen3.8 Flash Next之后&#xff0c;发现它有几个特点&#xff1a;参数量不算大&#xff0c;但上下文窗口可以拉…

作者头像 李华
网站建设 2026/10/15 22:37:08

LTX Desktop:开源本地4K视频生成与剪辑一体化工具

1. 项目概述&#xff1a;这不是又一个“AI剪辑玩具”&#xff0c;而是一次本地视频生产力的重新定义最近在几个技术社区里&#xff0c;陆续看到有人贴出LTX Desktop的实测截图——不是网页版&#xff0c;不是云端渲染队列&#xff0c;而是直接在一台i7-11800H RTX 3060笔记本上…

作者头像 李华