目录
1. 引言
在现代软件开发中,Python 以其简洁的语法和丰富的生态系统,成为了数据科学和机器学习领域的首选语言。然而,Python 在执行速度方面并不总是能够满足高性能计算的需求。C++ 则以其卓越的性能和强大的系统级编程能力,常用于性能要求极高的场景。Pybind11 是一个开源工具,旨在简化 C++ 与 Python 之间的交互,使得开发者可以在 Python 中方便地调用 C++ 编写的高性能代码。
1.1 Pybind11 简介
Pybind11 是一个轻量级的头文件库,允许在 C++11 及以上标准下,将 C++ 的函数、类和数据结构绑定到 Python 中。它的设计目标是替代 Boost.Python,但没有后者的编译复杂性和体积。通过 Pybind11,开发者可以:
- 高效地将 C++ 函数和类暴露给 Python:无需复杂的配置,即可在 Python 中调用 C++ 代码。
- 支持现代 C++ 特性:兼容 C++11 及以上标准,支持智能指针、模板、枚举等特性。
- 无缝集成:支持 numpy、Eigen 等常用库的数据类型转换。
1.2 为什么需要 Pybind11
在某些应用场景下,Python 的执行效率可能成为瓶颈,例如:
- 计算密集型任务:如大规模矩阵计算、图像处理、机器学习中的核心算法等。
- 实时系统:需要对性能有严格要求的系统,如高频交易、实时信号处理等。
- 遗留代码复用:已有大量用 C++ 编写的代码库,希望在 Python 项目中直接复用。
使用 Pybind11,可以将性能关键的部分用 C++ 编写,并通过简单的绑定在 Python 中调用,从而兼顾开发效率和运行效率。
2. 使用 Pybind11 进行 C++ 与 Python 交互
2.1 基本用法
Pybind11 的基本用法是通过编写 C++ 代码,将函数或类绑定到 Python 中。以下是一个简单的示例:
// example.cpp
#include <pybind11/pybind11.h>
int add(int i, int j) {
return i + j;
}
PYBIND11_MODULE(example, m) {
m.def("add", &add, "A function which adds two numbers");
}
在这个示例中,我们定义了一个简单的 add
函数,并使用 PYBIND11_MODULE
宏将其绑定到 Python 模块 example
中。
2.2 编译与生成共享库
为了在 Python 中使用我们编写的 C++ 模块,需要将其编译为共享库。不同的操作系统下,编译命令略有不同。
2.2.1 在 Linux 下编译
g++ -O3 -Wall -shared -std=c++11 -fPIC $(python3 -m pybind11 --includes) example.cpp -o example$(python3-config --extension-suffix)
命令解析:
g++
:GNU C++ 编译器。-O3
:最高级别的优化,提升运行效率。-Wall
:开启所有警告,帮助发现潜在问题。-shared
:生成共享库(.so
文件)。-std=c++11
:使用 C++11 标准。-fPIC
:生成与位置无关的代码,适用于共享库。$(python3 -m pybind11 --includes)
:获取 Pybind11 和 Python 的头文件路径。example.cpp
:源文件。-o example$(python3-config --extension-suffix)
:指定输出文件名,$(python3-config --extension-suffix)
获取适当的文件扩展名(如.so
)。
2.2.2 在 macOS 下编译
g++ -O3 -Wall -shared -std=c++11 -undefined dynamic_lookup $(python3 -m pybind11 --includes) example.cpp -o example$(python3-config --extension-suffix)
命令解析:
- 与 Linux 下基本相同,区别在于:
-undefined dynamic_lookup
:macOS 特有选项,允许在链接时未定义的符号在运行时解析。
2.2.3 编译选项详解
-
-O3
优化级别-O3
是编译器提供的最高级别优化选项,启用所有优化以提升代码运行效率。然而,需要注意的是,过度优化可能会导致编译时间增加,甚至在某些情况下引入不稳定性。 -
-Wall
警告信息-Wall
选项开启了编译器的所有常用警告,这有助于在编译阶段发现潜在的代码问题,提高代码质量。 -
-shared
生成共享库共享库(Linux 下为
.so
文件,macOS 下为.dylib
或.so
)可以被多个程序同时使用,节省内存和磁盘空间。在 Python 中,扩展模块通常以共享库的形式存在。 -
-std=c++11
指定标准指定使用 C++11 标准,使得我们可以在代码中使用现代 C++ 特性,如智能指针、自动类型推导等。
-
-fPIC
位置无关代码生成的位置无关代码可以在内存中任意位置加载,适用于共享库的生成。
-
-undefined dynamic_lookup
这是 macOS 下的特定选项,允许共享库在编译时不解析未定义的符号,而是在运行时解析。这对于动态语言的扩展模块非常有用。
2.3 在 Python 中使用编译后的模块
编译成功后,会生成一个共享库文件,例如 example.cpython-38-x86_64-linux-gnu.so
。在 Python 中,我们可以直接导入并使用:
import example
result = example.add(3, 4)
print(result) # 输出 7
3. 高级用法与注意事项
3.1 绑定类和复杂数据结构
除了函数,Pybind11 也支持将 C++ 类绑定到 Python 中。例如:
#include <pybind11/pybind11.h>
class Pet {
public:
Pet(const std::string &name) : name(name) {}
void setName(const std::string &name_) { name = name_; }
const std::string &getName() const { return name; }
private:
std::string name;
};
PYBIND11_MODULE(example, m) {
pybind11::class_<Pet>(m, "Pet")
.def(pybind11::init<const std::string &>())
.def("setName", &Pet::setName)
.def("getName", &Pet::getName);
}
在 Python 中:
import example
pet = example.Pet("Milo")
print(pet.getName()) # 输出 "Milo"
pet.setName("Otis")
print(pet.getName()) # 输出 "Otis"
3.2 与 NumPy 交互
Pybind11 提供了对 NumPy 数组的支持,可以方便地在 C++ 和 Python 之间传递数组。
#include <pybind11/pybind11.h>
#include <pybind11/numpy.h>
double sum_array(pybind11::array_t<double> input) {
auto buf = input.request();
double *ptr = (double *) buf.ptr;
size_t size = buf.size;
double sum = 0;
for (size_t idx = 0; idx < size; idx++)
sum += ptr[idx];
return sum;
}
PYBIND11_MODULE(example, m) {
m.def("sum_array", &sum_array, "Sum elements of a NumPy array");
}
在 Python 中:
import example
import numpy as np
arr = np.array([1.0, 2.0, 3.0])
result = example.sum_array(arr)
print(result) # 输出 6.0
3.3 随机数生成器的兼容性问题
在跨语言使用随机数生成器时,需要注意不同语言和库之间的兼容性问题。例如,C++11 提供了 std::mt19937
随机数生成器,而 NumPy 的 np.random.RandomState
则是 Python 中常用的随机数生成器。
问题描述:
np.random.RandomState
与std::mt19937
不兼容。这意味着,使用 Pybind11 将 C++ 的随机数生成器暴露给 Python 时,不能直接在 Python 中使用np.random.RandomState
来设置 C++ 中生成器的状态。
解决方案:
- 统一随机数生成器:在 C++ 和 Python 中使用相同的随机数生成算法和种子。
- 通过种子传递:让 Python 将种子传递给 C++,在 C++ 中初始化
std::mt19937
。 - 避免共享状态:将随机数的生成完全放在 C++ 或 Python 中,避免在两者之间共享生成器状态。
#include <pybind11/pybind11.h>
#include <random>
double random_double(int seed) {
std::mt19937 gen(seed);
std::uniform_real_distribution<> dis(0.0, 1.0);
return dis(gen);
}
PYBIND11_MODULE(example, m) {
m.def("random_double", &random_double, "Generate a random double");
}
在 Python 中:
import example
seed = 42
value = example.random_double(seed)
print(value)
通过在 Python 中控制种子,可以在 C++ 中生成可重复的随机数。
4. 一些🌰
为了更深入地理解 Pybind11 的使用,我们将结合实际例子,探讨如何在项目中应用 Pybind11。
4.1 高性能矩阵运算
假设我们需要对大型矩阵进行复杂的运算,而 Python 的性能无法满足需求。我们可以使用 C++ 编写核心计算逻辑,并通过 Pybind11 暴露给 Python。
#include <pybind11/pybind11.h>
#include <pybind11/numpy.h>
pybind11::array_t<double> matrix_add(pybind11::array_t<double> a, pybind11::array_t<double> b) {
auto buf_a = a.request(), buf_b = b.request();
if (buf_a.size != buf_b.size)
throw std::runtime_error("Input shapes must match");
double *ptr_a = (double *) buf_a.ptr;
double *ptr_b = (double *) buf_b.ptr;
pybind11::array_t<double> result(buf_a.size);
auto buf_result = result.request();
double *ptr_result = (double *) buf_result.ptr;
for (size_t idx = 0; idx < buf_a.size; idx++)
ptr_result[idx] = ptr_a[idx] + ptr_b[idx];
result.resize({buf_a.shape[0], buf_a.shape[1]});
return result;
}
PYBIND11_MODULE(example, m) {
m.def("matrix_add", &matrix_add, "Add two matrices");
}
在 Python 中:
import example
import numpy as np
a = np.ones((1000, 1000))
b = np.ones((1000, 1000))
result = example.matrix_add(a, b)
print(result)
通过这种方式,我们可以大幅提升矩阵运算的性能。
4.2 图像处理
在图像处理领域,性能也是关键因素。我们可以利用 C++ 的高性能和 OpenCV 等库,加速图像处理任务。
#include <pybind11/pybind11.h>
#include <pybind11/numpy.h>
#include <opencv2/opencv.hpp>
pybind11::array_t<uint8_t> convert_to_grayscale(pybind11::array_t<uint8_t> input) {
auto buf = input.request();
cv::Mat img(buf.shape[0], buf.shape[1], CV_8UC3, (uint8_t *) buf.ptr);
cv::Mat gray;
cv::cvtColor(img, gray, cv::COLOR_BGR2GRAY);
pybind11::array_t<uint8_t> result({buf.shape[0], buf.shape[1]}, gray.data);
return result;
}
PYBIND11_MODULE(example, m) {
m.def("convert_to_grayscale", &convert_to_grayscale, "Convert image to grayscale");
}
在 Python 中:
import example
import cv2
import numpy as np
img = cv2.imread('image.jpg')
gray = example.convert_to_grayscale(img)
cv2.imwrite('gray_image.jpg', gray)
4.3 处理大规模数据
在数据科学中,处理大规模数据时,经常需要优化代码性能。以下是一个计算大数组元素平方和的示例:
#include <pybind11/pybind11.h>
#include <pybind11/numpy.h>
double sum_of_squares(pybind11::array_t<double> input) {
auto buf = input.request();
double *ptr = (double *) buf.ptr;
size_t size = buf.size;
double sum = 0;
for (size_t idx = 0; idx < size; idx++)
sum += ptr[idx] * ptr[idx];
return sum;
}
PYBIND11_MODULE(example, m) {
m.def("sum_of_squares", &sum_of_squares, "Compute sum of squares");
}
在 Python 中:
import example
import numpy as np
data = np.random.rand(1000000)
result = example.sum_of_squares(data)
print(result)
Ref
[1] https://pybind11.readthedocs.io/en/stable/compiling.html#building-manually