pybind11 与 Eigen 矩阵互转指南:pybind11/eigen.h 的零拷贝机制、存储顺序与返回值策略详解
【免费下载链接】pybind11Seamless operability between C++11 and Python项目地址: https://gitcode.com/GitHub_Trending/py/pybind11
本文基于 pybind11 官方文档docs/advanced/cast/eigen.rst展开,系统讲解如何通过头文件pybind11/eigen.h在 C++ Eigen 矩阵类型与 numpy/scipy 数组之间实现透明转换。文中将覆盖按值传递的拷贝行为、Eigen::Ref零拷贝引用、返回值策略(reference_internal/copy)、行主序/列主序存储冲突的三种解决方案,以及py::arg().noconvert()强制“失败而非拷贝”的用法,并结合 include/pybind11/eigen/matrix.h 的类型转换器源码与 tests/test_eigen_matrix.cpp 测试用例,说明每一项规则在底层是如何落实的。
启用 Eigen 支持:只需要一个头文件
pybind11 对 Eigen 的 dense/sparse 线性代数类型提供“透明转换”支持:绑定函数时可以像处理普通 C++ 参数一样处理Eigen::MatrixXd、Eigen::Ref<...>、Eigen::SparseMatrix<...>等类型,Python 侧则直接使用 numpy 数组或 scipy 稀疏矩阵,无需手写任何转换胶水代码。
启用方式非常直接——在绑定代码中包含可选头文件 pybind11/eigen.h:
#include <pybind11/eigen.h>从源码结构看,include/pybind11/eigen.h 本体只有一行实质内容#include "eigen/matrix.h",全部转换逻辑都在 include/pybind11/eigen/matrix.h 中实现,并依赖 include/pybind11/numpy.h 的 numpy 数组能力。该文件开头有一处硬性版本约束(见 matrix.h#L36-L37):
static_assert(EIGEN_VERSION_AT_LEAST(3, 2, 7), "Eigen matrix support in pybind11 requires Eigen >= 3.2.7");即Eigen 版本必须不低于 3.2.7,否则直接编译失败(原因注释写明:早于 3.2.7 的 Eigen 移动构造函数不正确,而矩阵拷贝代价高昂,不愿引入额外拷贝)。此外源码中还有一处与版本相关的实现差异:Eigen ≥ 3.3.0 使用Eigen::Map<Eigen::SparseMatrix<...>>构造稀疏矩阵映射,更早版本回退到Eigen::MappedSparseMatrix(见 matrix.h#L52-L60)。
构建系统层面,pybind11 的测试工程展示了如何探测 Eigen 依赖(见 tests/CMakeLists.txt#L302-L314):优先尝试find_package(Eigen3 ... CONFIG),找不到时回退到自带的 tools/FindEigen3.cmake 模块(MODULE 模式,兼容旧版 Eigen 3 的纯头文件安装);也可以打开-DDOWNLOAD_EIGEN=ON让 CMake 自动下载指定版本。在自己的 CMake 工程里,通常find_package(Eigen3)后把Eigen3::Eigen目标链接到模块即可。
按值传递:自动拷贝,最省心但最昂贵
当绑定的函数使用普通 Eigen 稠密对象作参数(例如Eigen::MatrixXd)时,pybind11 的行为是:
- 接受任何“已经是、或可转换为”
numpy.ndarray且维度与 Eigen 类型兼容的输入; - 把数据拷贝进一个合适类型的临时 Eigen 对象;
- 用这个临时对象调用 C++ 函数。
稀疏矩阵同理:数据从/往scipy.sparse.csr_matrix/scipy.sparse.csc_matrix对象之间拷贝。
在源码中,这条路径由type_caster特化is_eigen_dense_plain<Type>(普通稠密矩阵)的load()实现(见 matrix.h#L299-L342):先array::ensure(src)把输入强制成数组,再调用props::conformable(buf)检查维度是否装得进该 Eigen 类型,然后分配Type value(fits.rows, fits.cols),最后用 numpy 的PyArray_CopyInto_完成数据拷贝(类型转换也由这一步顺带完成)。这解释了文档的两个结论:按值传递总是产生拷贝;且输入可以是整数数组——目标类型是 double 时会自动做类型转换。
稀疏类型的转换则体现在is_eigen_sparse<Type>特化(见 matrix.h#L661-L714):load()会 importscipy.sparse,按 Eigen 类型的行/列主序选择csr_matrix或csc_matrix,读取其data/indices/indptr三个数组构造EigenMapSparseMatrix;cast()返回时先makeCompressed(),再把(data, indices, indptr)三元组建回 scipy 稀疏对象。
按引用传递:用 Eigen::Ref 实现零拷贝
按值传递的主要局限是:每次数据转换隐含一次拷贝——大矩阵下代价高昂,而且无法绑定“会修改矩阵参数”的函数。pybind11 的解法与在 Eigen 中写泛型函数一样:使用Eigen::Ref<MatrixType>。
Eigen::Ref<const MatrixType>(只读引用):pybind11 会尽力避免拷贝,通过Eigen::Map直接映射进源numpy.ndarray的数据。前提是两者都满足:
- 数据类型一致(例如
dtype='float64'对应MatrixType::Scalar = double); - 存储布局兼容(下面“存储顺序”一节会详细展开,默认情况下 numpy 与 Eigen并不兼容)。
若无法满足(类型不同,如把整型数组传给需要 double 的参数;或存储不兼容),pybind11 退化为生成临时拷贝再传入。
Eigen::Ref<MatrixType>(可写引用,无const):pybind11 只允许在“可以映射”且 numpy 数组可写(a.flags.writeable为 True)时才调用;对传入变量的任何访问与修改都会透明地直接落在numpy.ndarray上。因此下面这段代码可以按预期工作:
void scale_by_2(Eigen::Ref<Eigen::VectorXd> v) { v *= 2; }注意:由于 numpy 与 Eigen 默认存储顺序不同,你多半会撞上限制,处理办法见下文 py::EigenDRef。
稀疏类型不支持按引用传递。
这段规则在Eigen::Ref专用 type_caster 的load()中体现得非常清楚(见 matrix.h#L499-L553):
- 先用
bool need_copy = !isinstance<Array>(src)判断输入是否已是满足布局要求的数组; - 不是则检查
aref.writeable()(可写引用)与stride_compatible<props>()(步进兼容); - 一旦需要拷贝,则
if (!convert || need_writeable) return false;——即可写引用绝不允许用临时拷贝,只能失败; - 成功路径是构造
Eigen::Map并包成Ref(map.reset(new MapType(...)); ref.reset(new Type(*map));),整个过程中没有任何元素级拷贝。
测试文件 tests/test_eigen_matrix.cpp 中的add_cm/add_rm/add_any与_block用例专门验证这一点:C++ 侧对Eigen::Ref的写操作必须能反映到 Python 侧同一个 numpy 数组里,_block甚至通过“先改 Python 侧元素再读 C++ 侧”的方式主动检测是否发生了意外拷贝(见 test_eigen_matrix.cpp#L206-L231)。
返回给 Python:零拷贝、写保护与生命周期
返回普通稠密 Eigen 矩阵(如Eigen::MatrixXd、Eigen::RowVectorXf)时,pybind11 会把矩阵封装起来,返回一个直接引用该 Eigen 矩阵数据的 numpy 数组——不拷贝数据。此时array.flags.owndata为False,Eigen 对象的生命周期与返回的数组绑定。源码中这一步是eigen_encapsulate():用 capsule 包住new出来的矩阵,capsule 销毁时delete矩阵(见 matrix.h#L284-L288)。
返回非引用的const类型(如const Eigen::MatrixXd)时行为相同,只是 pybind11 会额外把 numpy 数组的writeable标志置为 False(见 matrix.h#L371-L374 与eigen_ref_array中对std::is_const的判断)。
返回左值引用或指针时,遵循 pybind11 通用返回值策略(return value policies):默认情况下,左值引用会被拷贝,指针则由 pybind11 管理。要避免拷贝,需要显式指定策略:
class MyClass { Eigen::MatrixXd big_mat = Eigen::MatrixXd::Zero(10000, 10000); public: Eigen::MatrixXd &getMatrix() { return big_mat; } const Eigen::MatrixXd &viewMatrix() { return big_mat; } }; // 稍后,在绑定代码中: py::class_<MyClass>(m, "MyClass") .def(py::init<>()) .def("copy_matrix", &MyClass::getMatrix) // Makes a copy! .def("get_matrix", &MyClass::getMatrix, py::return_value_policy::reference_internal) .def("view_matrix", &MyClass::viewMatrix, py::return_value_policy::reference_internal) ;a = MyClass() m = a.get_matrix() # flags.writeable = True, flags.owndata = False v = a.view_matrix() # flags.writeable = False, flags.owndata = False c = a.copy_matrix() # flags.writeable = True, flags.owndata = True # m[5,6] 和 v[5,6] 指向同一元素,c[5,6] 不是。此例中py::return_value_policy::reference_internal的作用是把 MyClass 对象的生命周期挂到返回的数组上,防止数组还在被引用时 C++ 对象先被析构。对应源码分支见 matrix.h#L346-L398:automatic/automatic_reference对左值引用一律改写为copy,reference_internal则走eigen_ref_array<props>(*src, parent)带上父对象引用。
返回Eigen::Ref、Eigen::Map或任何映射到稠密矩阵的对象(例如matrix.block()的返回值)时,pybind11 默认行为是直接引用其数据——你必须自己保证这份数据继续有效。若担心悬垂,有两种手段:
- 绑定函数时加
py::return_value_policy::copy,明确要求拷贝; - 或用
py::return_value_policy::reference_internal/py::keep_alive,让被映射的数据活得和返回的 numpy 数组一样久。
返回这类引用/映射时,pybind11 还会尊重其只读属性:若 Ref/Map 本身只读,返回的 numpy 数组会被标记为不可写。这由eigen_map_caster::cast()完成(见 matrix.h#L424-L445),写保护判断依据is_eigen_mutable_map<MapType>特征。
稀疏类型在返回时总是被拷贝。
存储顺序:零拷贝的最大陷阱与三种解法
通过Eigen::Ref传参有一个必须了解的限制:默认Eigen::Ref<MatrixType>要求沿列方向连续存储(列主序类型,Eigen 的默认),或当MatrixType是Eigen::RowMajor时沿行方向连续存储。而 numpy 的默认是行主序——因此默认情况下你无法把 numpy 数组按引用传给 Eigen,必须做下面两处改动之一。
(注意:向量、行矩阵或列矩阵不受此限——一维为 1 时“行主序/列主序”的区分没有意义。)
方案一:动态步长引用 py::EigenDRef
把Eigen::Ref<MatrixType>换成更通用的Eigen::Ref<MatrixType, 0, Eigen::Stride<Eigen::Dynamic, Eigen::Dynamic>>(或任何第三模板参数为完全动态 stride 的等价类型)。这个类型相当冗长,pybind11 因此在 matrix.h#L43-L48 提供了别名:
using EigenDStride = Eigen::Stride<Eigen::Dynamic, Eigen::Dynamic>; template <typename MatrixType> using EigenDRef = Eigen::Ref<MatrixType, 0, EigenDStride>; template <typename MatrixType> using EigenDMap = Eigen::Map<MatrixType, 0, EigenDStride>;它允许 Eigen 映射任意存储顺序。之所以不作为 Eigen 默认,是性能考虑:连续存储才能做 SIMD 向量化,编译期未知连续性的存储做不到。默认的Eigen::Refstride 允许外维(列主序矩阵的行、行主序矩阵的列)非连续,但内维必须连续;EigenDRef则内外维都放开。
它还有个额外好处:能映射 numpy 数组切片。文档给的(刻意造出来的)例子是“把偶数行(0、2、4…)且位于第 2、5、8 列的元素全部乘 2”:
m.def("scale", [](py::EigenDRef<Eigen::MatrixXd> m, double c) { m *= c; });# a = np.array(...) scale_by_2(myarray[0::2, 2:9:3])测试文件中的add_any与even_rows/even_cols/diagonal用例分别验证了任意 stride 的写入和py::EigenDMap返回非连续子视图的场景(见 test_eigen_matrix.cpp#L134-L194)。
方案二:改 C++ 类型或 numpy 数组的存储顺序
更侵入式的办法是让底层数据根本不发生非连续存储问题。典型做法是在合适处使用Eigen::RowMajor矩阵:
using RowMatrixXd = Eigen::Matrix<double, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor>; // 用 RowMatrixXd 代替 MatrixXd这样接受Eigen::Ref<RowMatrixXd>的绑定函数可以直接用 numpy 默认数组调用,无需拷贝。
或者反过来,创建数组时加order='F'让 numpy 采用列主序:
myarray = np.array(source, order="F")这样的对象可以传给接受Eigen::Ref<MatrixXd>(或任何列主序 Eigen 类型)的绑定函数。
两个重要的注意点:
- 不能简单地把所有 Eigen/numpy 用法从一个顺序翻转到另一个:某些操作会改变 numpy 数组的存储顺序。例如
a2 = array.transpose()得到的a2是array的视图、引用同一份数据,但存储顺序恰好相反! - 该方案允许 Eigen 做完全优化的向量化计算,但不能用于数组切片(与方案一相反)。
返回矩阵给 Python 时则无需任何特殊存储考虑:无论 C++ 侧什么存储顺序,pybind11 创建的 numpy 数组都会带上 numpy 正确解读该数组所需的 stride。这在eigen_array_cast()中体现为用rowStride()/colStride()逐维计算 numpy 步长(见 matrix.h#L248-L267)。
失败而非拷贝:py::arg().noconvert()
Eigen::Ref<const MatrixType>的默认行为是:当传入的 numpy 数组元素类型不符或 stride 布局不兼容时,拷贝矩阵值。如果你要明确禁止拷贝,应在绑定参数时使用py::arg().noconvert()标注(用法详见 nonconverting arguments)。
下面是一个“不允许发生数据拷贝”的参数示例:
// 要绑定的方法与函数: class MyClass { // ... double some_method(const Eigen::Ref<const MatrixXd> &matrix) { /* ... */ } }; float some_function(const Eigen::Ref<const MatrixXf> &big, const Eigen::Ref<const MatrixXf> &small) { // ... } // 对应的绑定代码: using namespace pybind11::literals; // for "arg"_a py::class_<MyClass>(m, "MyClass") // ... 其他类定义 .def("some_method", &MyClass::some_method, py::arg().noconvert()); m.def("some_function", &some_function, "big"_a.noconvert(), // <- 此参数禁止拷贝 "small"_a // <- 必要时可以拷贝 );在上述绑定下,对MyClass对象调用some_method(m)、或调用some_function(m, m2)时,若数据需要拷贝就会抛出RuntimeError而非创建临时数组;m2对应的small参数则允许在必要时拷贝。
实现上,noconvert()会在第一轮重载解析中以convert=false调用load();对Ref<const MatrixType>而言,一旦布局不兼容需要拷贝,load()中if (!convert || need_writeable) return false;直接拒绝(见 matrix.h#L525-L531),pybind11 随后将其包装为TypeError/RuntimeError报告给 Python。
注意:对可写Eigen 引用(如Eigen::Ref<MatrixXd>,MatrixXd上没有const),无需显式.noconvert()——可写引用永远不会用临时拷贝调用,不兼容即失败。
向量与行列矩阵的维度语义
Eigen 与 numpy 对“向量”的概念有本质差异:
- Eigen 中,向量就是编译期把行数或列数固定为 1的矩阵(列向量或行向量);
- numpy 除 2 维的 1xN 与 Nx1 数组外,还有真正的1 维N 长度数组。
Python → C++ 方向:
- 传 2 维 1xN 或 Nx1 数组时,Eigen 类型维度必须严格匹配:不能把 2 维 Nx1 numpy 数组传给期望行向量的 Eigen 参数,也不能把 1xN 数组当列向量传;
- 传1 维N 长度数组时,pybind11 会尽量适配:若 Eigen 类型能容纳长度 N 的列向量,就按列向量传入;否则若类型约束接受行向量,就按行向量传入(两者都支持时列向量优先,例如 1D 数组传给
MatrixXd参数时会成为列向量); - Eigen 类型不必显式是向量类型:长度为 5 的 1D numpy 数组传给
Eigen::Matrix<double, Dynamic, 5>会形成 1x5 矩阵;传给Eigen::MatrixXd则形成 5x1 矩阵。
这些规则由EigenProps::conformable()静态实现(见 matrix.h#L173-L218),它按“编译期向量 / 固定列数 / 完全动态”三种分支决定 1D 输入最终落成 (n,1)、(1,n) 或 (1,n) 的形状。
C++ → Python 方向:返回 Eigen 向量到 numpy 存在歧义——长度 4 的行向量既可以是长度 4 的 1D 数组,也可以是 1x4 的 2D 数组。pybind11 的折中是看返回的 Eigen 类型:
- 若它是编译期向量(行数或列数在编译期固定为 1),返回 1D numpy 数组;
- 若是仅运行期为向量(如
MatrixXd、Matrix<float, Dynamic, 4>的 1xN 实例),返回 2D 数组。
不满意时可用array.reshape(...)拿到同一份数据的期望维度视图。
延伸阅读与测试验证
- 完整的 C++ 侧示例见 tests/test_eigen_matrix.cpp 与对应的 tests/test_eigen_matrix.py,覆盖了上述按值/按引用、存储顺序、返回值策略的全部规则;文档 docs/advanced/cast/eigen.rst 末尾提到的
tests/test_eigen.cpp即对应该文件(现按矩阵/张量拆分为test_eigen_matrix.cpp与test_eigen_tensor.cpp)。 - 张量(
Eigen::Tensor)的转换由 include/pybind11/eigen/tensor.h 单独提供,示例见 tests/test_eigen_tensor.cpp。 - 返回值策略的完整语义参考 docs/advanced/cast/index.rst;
noconvert()的通用用法参考 docs/advanced/cast/overview.rst。
【免费下载链接】pybind11Seamless operability between C++11 and Python项目地址: https://gitcode.com/GitHub_Trending/py/pybind11
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考