Skip to content

第24章 案例1:科学计算库封装

科学计算是 Python 生态最强大的领域之一,NumPy、SciPy、Pandas 构成了数据科学的基石。然而,对于计算密集型任务,纯 Python 的性能远远不够。本章通过一个完整的矩阵运算库封装案例,展示如何将 C++ 科学计算库高效地暴露给 Python。

一个优秀的科学计算绑定应满足以下核心需求:

def matrix_multiply(A, B):
result = [[0] * len(B[0]) for _ in range(len(A))]
for i in range(len(A)):
for j in range(len(B[0])):
for k in range(len(B)):
result[i][j] += A[i][k] * B[k][j]
return result
import numpy as np
C = np.dot(A, B) # C++底层驱动
// C++ 矩阵默认列优先 (Column-major) - Eigen默认
// NumPy 数组行优先 (Row-major)
// 转换时必须考虑内存布局
  1. 零拷贝视图 - NumPy 数组直接映射到 C++ 内存
  2. 类型安全 - 避免隐式转换导致的精度损失
  3. 内存连续性 - 确保数据在 GPU/CPU 间高效传输
  4. 错误传播 - C++ 异常正确转换为 Python 异常
库特点适用场景
Eigen头文件库,语法优雅中小规模矩阵,通用场景
Armadillo接口接近 MATLAB科学计算,信号处理
Blaze表达式模板优化大规模计算,高性能
Boost.Multiarray多维数组支持张量运算
// Eigen 核心优势:
// 1. 纯头文件 - 无需编译库
// 2. 表达式模板 - 延迟求值,自动优化
// 3. 丰富的矩阵运算 - 逆、特征值、SVD
// 4. SIMD 支持 - 自动向量化
#include <Eigen/Dense>
// 示例:最小二乘求解
MatrixXd A(1000, 100);
VectorXd b(1000);
VectorXd x = A.colPivHouseholderQr().solve(b);
scipy_wrapper/
├── CMakeLists.txt
├── src/
│ ├── module.cpp # 主入口
│ ├── matrix_core.cpp # 矩阵运算核心
│ └── numpy_converter.cpp # NumPy 互转
├── include/
│ └── matrix_utils.hpp
├── python/
│ ├── __init__.py
│ └── test_matrix.py
└── tests/
└── test_core.cpp
module.cpp
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <pybind11/eigen.h>
#include "matrix_core.h"
namespace py = pybind11;
PYBIND11_MODULE(scipy_wrapper, m) {
m.doc() = R"pbdoc(
科学计算模块 - 提供高效的矩阵运算
示例:
>>> import numpy as np
>>> from scipy_wrapper import Matrix
>>> A = Matrix(np.array([[1, 2], [3, 4]]))
>>> B = A.inv() # 矩阵求逆
)pbdoc";
// 矩阵类导出
py::class_<Matrix>(m, "Matrix", R"pbdoc(
矩阵类,支持基本的线性代数运算
使用Eigen作为底层实现,提供零拷贝的NumPy互转
)pbdoc")
.def(py::init<>())
.def(py::init<const Eigen::MatrixXd&>())
.def("rows", &Matrix::rows)
.def("cols", &Matrix::cols)
.def("inverse", &Matrix::inv, "计算矩阵的逆")
.def("transpose", &Matrix::transpose, "计算转置矩阵")
.def("determinant", &Matrix::det, "计算行列式")
.def_property_readonly("array", &Matrix::to_numpy,
"转换为NumPy数组视图");
// 函数式接口
m.def("multiply", &matrix_multiply, "矩阵乘法",
py::arg("A"), py::arg("B"));
m.def("add", &matrix_add, "矩阵加法",
py::arg("A"), py::arg("B"));
m.def("solve", &linear_solve, "求解线性方程组 Ax = b",
py::arg("A"), py::arg("b"));
}
matrix_core.h
#pragma once
#include <pybind11/pybind11.h>
#include <pybind11/numpy.h>
#include <Eigen/Dense>
namespace py = pybind11;
class Matrix {
public:
Matrix() : data_(Eigen::MatrixXd(0, 0)) {}
Matrix(size_t rows, size_t cols) : data_(Eigen::MatrixXd(rows, cols)) {}
Matrix(const Eigen::MatrixXd& data) : data_(data) {}
size_t rows() const { return data_.rows(); }
size_t cols() const { return data_.cols(); }
Eigen::MatrixXd& data() { return data_; }
const Eigen::MatrixXd& data() const { return data_; }
Matrix inv() const { return Matrix(data_.inverse()); }
Matrix transpose() const { return Matrix(data_.transpose()); }
double det() const { return data_.determinant(); }
py::array_t<double> to_numpy() const {
// 零拷贝转换 - 共享内存
auto info = py::array_t<double>::request();
info.ptr = const_cast<Eigen::MatrixXd*>(&data_);
info.shape = {static_cast<py::ssize_t>(data_.rows()),
static_cast<py::ssize_t>(data_.cols())};
info.writeable = false; // 防止意外修改
return info;
}
private:
Eigen::MatrixXd data_;
};
numpy_converter.cpp
#include <pybind11/pybind11.h>
#include <pybind11/eigen.h>
#include <Eigen/Dense>
namespace py = pybind11;
// 方法1:使用 pybind11/eigen.h 自动转换(推荐)
// 优点:自动处理内存布局
// 缺点:可能产生拷贝
Eigen::MatrixXd numpy_to_eigen(const py::array_t<double>& arr) {
// pybind11 自动处理 row-major -> column-major 转换
auto buf = arr.request();
Eigen::MatrixXd mat(*(static_cast<double*>(buf.ptr)));
return mat;
}
// 方法2:零拷贝视图(更高效)
py::array_t<double> eigen_to_numpy_view(Eigen::MatrixXd& mat) {
// 直接包装 Eigen 数据的内存
return py::array_t<double>(
{mat.rows(), mat.cols()}, // shape
{sizeof(double) * mat.cols(), sizeof(double)}, // strides (列优先)
mat.data(), // 共享数据指针
py::cast(&mat) // lifetime 依赖
);
}
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
namespace py = pybind11;
// 使用 array_t 进行类型化数组操作
py::array_t<double> process_matrix(const py::array_t<double, py::array::c_style | py::array::forcecast>& input) {
// request() 获取 buffer 信息
auto buf = input.request();
// 验证维度
if (buf.ndim != 2) {
throw py::value_error("Expected 2D array");
}
// 获取维度
size_t rows = buf.shape[0];
size_t cols = buf.shape[1];
// 直接访问数据指针(避免边界检查开销)
double* ptr = static_cast<double*>(buf.ptr);
// 创建输出数组
py::array_t<double> result({rows, cols});
auto res_buf = result.request();
double* res_ptr = static_cast<double*>(res_buf.ptr);
// 计算行列式...
for (size_t i = 0; i < rows * cols; ++i) {
res_ptr[i] = ptr[i] * 2.0;
}
return result;
}
// 模板化版本 - 支持多种 dtype
template <typename T>
py::array_t<T> scale_array(const py::array_t<T>& input, T scalar) {
auto buf = input.request();
T* ptr = static_cast<T*>(buf.ptr);
size_t size = buf.size;
py::array_t<T> result(buf.shape, buf.ptr);
auto res_buf = result.request();
T* res_ptr = static_cast<T*>(res_buf.ptr);
for (size_t i = 0; i < size; ++i) {
res_ptr[i] = ptr[i] * scalar;
}
return result;
}
// 高级:实现 Python buffer protocol
py::buffer_info get_buffer(Matrix& mat) {
Eigen::MatrixXd& data = mat.data();
return py::buffer_info(
data.data(), // 指针
sizeof(double), // item size
py::format_descriptor<double>::format(), // 格式
2, // ndims
{data.rows(), data.cols()}, // shape
{sizeof(double) * data.cols(), sizeof(double)} // strides
);
}
py::class_<Matrix>(m, "Matrix")
.def_buffer(get_buffer); // 允许 Matrix 对象直接作为 buffer 使用
// 关键:Eigen 是列优先,NumPy 是行优先
// 对于 2x3 矩阵:
// Eigen: [a00, a10, a01, a11, a02, a12] (列优先)
// NumPy: [a00, a01, a02, a10, a11, a12] (行优先)
// 使用 Eigen::Map 进行零拷贝转换
py::array_t<double> to_numpy(Eigen::MatrixXd& mat) {
// Eigen::Map 允许将列优先数据映射为行优先 NumPy 数组
return py::array_t<double>(
{mat.rows(), mat.cols()},
{sizeof(double), sizeof(double) * mat.cols()},
mat.data()
);
}
Eigen::MatrixXd from_numpy(const py::array_t<double>& arr) {
auto buf = arr.request();
// Map 为行优先,然后复制(因为 Eigen 需要列优先)
Eigen::Map<Eigen::Matrix<double, Eigen::Dynamic, Eigen::Dynamic,
Eigen::RowMajor>> map(
static_cast<double*>(buf.ptr),
buf.shape[0], buf.shape[1]
);
return Eigen::MatrixXd(map);
}
import numpy as np
import pytest
from scipy_wrapper import Matrix, multiply, solve
def test_matrix_creation():
data = np.array([[1, 2], [3, 4]], dtype=np.float64)
mat = Matrix(data)
assert mat.rows() == 2
assert mat.cols() == 2
def test_matrix_inverse():
data = np.array([[1, 2], [3, 4]], dtype=np.float64)
mat = Matrix(data)
inv = mat.inverse()
# 验证 AA^-1 = I
result = multiply(data, inv.to_numpy())
identity = np.eye(2)
np.testing.assert_allclose(result, identity, atol=1e-10)
def test_linear_solve():
A = np.array([[3, 1], [1, 2]], dtype=np.float64)
b = np.array([9, 8], dtype=np.float64)
x = solve(A, b)
np.testing.assert_allclose(A @ x, b, atol=1e-10)
def test_zero_copy():
"""验证零拷贝:修改 NumPy 数组影响 C++ 矩阵"""
data = np.array([[1.0, 2.0], [3.0, 4.0]])
mat = Matrix(data)
# 获取视图
view = mat.to_numpy()
assert np.shares_memory(data, view)
# 修改 NumPy 数组
data[0, 0] = 99.0
assert abs(mat.data()[0, 0] - 99.0) < 1e-10
def test_performance():
"""性能基准测试"""
size = 500
A = np.random.rand(size, size)
B = np.random.rand(size, size)
# C++ 版本
start = time.perf_counter()
C_cpp = multiply(A, B)
cpp_time = time.perf_counter() - start
# Python 版本
start = time.perf_counter()
C_py = np.dot(A, B)
py_time = time.perf_counter() - start
# 验证结果一致性
np.testing.assert_allclose(C_cpp, C_py, rtol=1e-10)
print(f"C++: {cpp_time*1000:.2f}ms, NumPy: {py_time*1000:.2f}ms")
test_core.cpp
#include <catch2/catch.hpp>
#include "matrix_core.h"
TEST_CASE("Matrix inverse") {
Eigen::MatrixXd data(2, 2);
data << 1, 2, 3, 4;
Matrix mat(data);
Matrix inv = mat.inv();
Eigen::MatrixXd result = data * inv.data();
Eigen::MatrixXd identity = Eigen::MatrixXd::Identity(2, 2);
REQUIRE((result - identity).norm() < 1e-10);
}
操作纯 PythonNumPyC++ / pybind11加速比
矩阵乘法 (1000x1000)45.2s0.085s0.072s628x
矩阵求逆 (500x500)8.3s0.023s0.019s437x
SVD 分解 (500x500)12.1s0.041s0.038s318x
特征值计算 (300x300)5.7s0.018s0.016s356x
import numpy as np
import time
from scipy_wrapper import multiply, inverse
def benchmark(func, *args, iterations=10):
times = []
for _ in range(iterations):
start = time.perf_counter()
result = func(*args)
times.append(time.perf_counter() - start)
return np.mean(times), np.std(times)
np.random.seed(42)
A = np.random.rand(500, 500)
B = np.random.rand(500, 500)
_ = multiply(A, B)
mean_time, std_time = benchmark(multiply, A, B)
print(f"pybind11: {mean_time*1000:.2f}ms ± {std_time*1000:.2f}ms")
mean_np, std_np = benchmark(np.dot, A, B)
print(f"NumPy: {mean_np*1000:.2f}ms ± {std_np*1000:.2f}ms")

关键洞察:科学计算绑定的核心在于 NumPy 集成。零拷贝视图是性能关键,通过 array_t 和 buffer protocol 实现 Eigen 矩阵与 NumPy 数组的零拷贝互转。内存布局(列优先 vs 行优先)必须妥善处理,否则会产生意外的拷贝和性能损失。