第 34 章 · 性能优化与别名陷阱 这一章能帮你少踩 Eigen 最经典的坑别名问题、auto 陷阱以及一系列性能优化技巧。34.1 别名问题左右两边共用内存别名aliasing赋值语句左右两边引用同一块内存时可能边读边写导致错误。Eigen 只在一部分场景自动帮你兜底剩下的要你自己判断。下面三档都是本书的实测结论Eigen 5.0 MinGW GCC 16.1-O0与-O2结果一致别背成口诀要理解为什么。第一档Eigen 自动处理随便写aa*a;// 矩阵乘法自动用临时变量安全a2*a;// 标量乘法安全aa*a.transpose();// 矩阵乘法里带转置同样安全实测 5 11 / 11 25aa.array().square();// 只有一个操作数的逐元素自运算实测安全aa.array().exp();// abs / sqrt / min(标量) 等单目运算同上vv.array().square();// 向量版也安全纠正一个流传很广的说法很多资料写逐元素自运算不安全必须加.eval()。实测并非如此——单操作数的square()、exp()、abs()无论Matrix还是Array、无论哪个优化级别结果都和加.eval()的版本逐元素相同如a为 1 2 / 3 4 时平方得 1 4 / 9 16。加.eval()不会出错只是多一次拷贝但别把不加就错当规则背那会让你把注意力放错地方。真正危险的是第二个操作数带转置的情形见下面两档。第二档Debug 会断言Release 静默算错tt.transpose();// Debug断言中止mmm.transpose();// Debug断言中止m.block2,2(0,0)m.block2,2(0,0).transpose();// Debug断言中止断言原文Eigen 自己的文件别去改它Assertion failed: (...) aliasing detected during transposition, use transposeInPlace() or evaluate the rhs into a temporary using .eval(), file Eigen/src/Core/Transpose.h, line 402换编译器再验一遍同一份代码用 MSVC 的cl /std:c17 /EHsc /utf-8Visual Studio 2022编译断言原文一模一样只有行号是Transpose.h, line 406不同编译器展开模板的行数略有差别。两个独立编译器给出同一个结论这条规则不是 MinGW 的个例。关键坑这个检查靠assert实现而 Release 构建定义了NDEBUG断言会被整个编译掉程序继续跑并给出错误结果。实测t t.transpose()初始t为 1 2 / 3 4构建方式实际输出正确结果Debug保留断言断言中止告诉你怎么办—Release-O2 -DNDEBUG1 2 / 2 4❌1 3 / 2 4MSVC Release/O2 /DNDEBUG1 2 / 2 4❌与 g 一致1 3 / 2 4Debug -DEIGEN_NO_DEBUG断言消失1 2 / 2 4❌1 3 / 2 4也就是说只在 Release 里试过、没报错不等于安全。关于EIGEN_NO_DEBUGEigen 的这批检查全靠assert所以只要 assert 被关掉保护就一起没了——不管是-DNDEBUGRelease还是显式定义EIGEN_NO_DEBUG。实测在-O0、没加-DNDEBUG的 Debug 构建上定义EIGEN_NO_DEBUG转置自赋值不再中止直接给你错的1 2 / 2 4第 26 章 26.3 讲的动态矩阵尺寸不匹配也一样断言消失、静默返回错误结果。除非你在排查 Eigen 内部行为不要定义这个宏。两种正确写法t.transposeInPlace();// ✅ 原地转置就是为这种自赋值设计的t2t2.transpose().eval();// ✅ 先把右边结算成一个独立结果第三档连 Debug 都不报一直静默算错Eigen::Matrix2d a;a1,2,3,4;aa.array()*a.transpose().array();// 无警告、无断言实测-O0和-O2都打印1 12 6 16而正确答案是1 6 / 6 16元素 (0,1) 乘的是已经被改写过的a(1,0)。更值得注意的是同一句话把Matrix2d换成ArrayXXd就会触发第二档的断言——Eigen 的别名检查不覆盖所有写法没报错不能作为安全证明。修法与第二档相同整块包住更保险实测两种写法在 Debug/Release 下都能得到正确的1 6 / 6 16a(a.array()*a.transpose().array()).eval();// ✅ 整个右先算成临时结果Eigen::Matrix2d tmpa.array()*a.transpose().array();// ✅ 或借助临时变量atmp;保命规则右边出现transpose()/adjoint()/block且和左边共用内存 → 一律.eval()或改用xxxInPlace()如transposeInPlace()。拿不准就先赋给一个临时变量再写回。一定要用 Debug 构建跑一遍——断言只在 Debug 生效Release 只会静默给你错数。记忆矩阵乘法自动安全单操作数的逐元素自运算实测安全只要右边带转置就要.eval()或用InPlace因为 Release 下没有任何人会提醒你。34.2 noalias()跳过临时变量确定左右无重叠时用.noalias()告诉 Eigen “没有别名”省掉临时变量更快C.noalias()A*B;// 已知 C 与 A、B 无重叠直接写入默认C A * B会用临时变量保证安全多一次拷贝确定安全后加.noalias()提速。34.3 auto 陷阱不要把矩阵表达式直接赋给autoEigen::Matrix2d m;m1,2,3,4;// ❌ auto 捕获的是表达式模板不是计算结果autoexprm.transpose();m5,6,7,8;// 改 m 后expr 的结果会跟着变// ✅ 用明确类型Eigen::Matrix2d goodm.transpose();// ✅ 或加 .eval() 强制求值autosafem.transpose().eval();auto捕获的是惰性表达式记录了m 的转置这个动作m 一变结果就变。用明确类型或.eval()就安全了。34.4 编译优化最重要Eigen 是模板库不开优化会慢几十倍。这是最关键的设置g-stdc17-O2-I你的Eigen目录main.cpp-omain# g 开启优化cl /std:c17 /O2 /EHsc /utf-8 /I你的Eigen目录main.cpp# MSVCCMake 则用 Release 配置附录 C.6 的CMakeLists.txt已在单配置生成器下默认 Releasecmake-S.-Bbuild-DCMAKE_BUILD_TYPERelease cmake--buildbuild--configRelease# Visual Studio 这类多配置生成器必须加 --config实测提醒Eigen 5.0 MinGW g 16.1程序是 500×500 双精度随机矩阵连乘 5 次编译选项实测耗时相对-O0-O0CMake 的 Debug 就是这档约 2400 ms1×-O1约 66 ms快约 36 倍-O2约 65 ms快约 37 倍-O3约 65 ms快约 37 倍与-O2无差别两个结论一是不开优化慢几十倍不是夸张实测就是37 倍二是-O1已经把 Eigen 需要的优化拿满了-O3相对-O2没有任何额外收益所以本书统一用-O2MSVC 用/O2不必去追-O3。换一台机器绝对数字会变但量级不会变。做性能对比前务必确认自己是 Release/带优化——否则你测的是编译器的优化能力不是 Eigen 的性能。34.5 性能要点清单要点说明开编译优化-O2/ Release最重要按引用传递函数参数用const MatrixXd避免复制大矩阵固定大小类型小矩阵用Matrix3d而非MatrixXd复用分解solver.compute(A)放循环外循环内只solve()noalias确定无别名时用省临时变量表达式合并尽量写成一个表达式减少中间临时变量34.6 按引用传递函数参数// ❌ 按值传递每次调用都要复制整个矩阵慢doublesumCopy(Eigen::MatrixXd m){returnm.sum();}// ✅ 按常量引用传递零拷贝快且安全doublesumRef(constEigen::MatrixXdm){returnm.sum();}两个函数故意起了不同的名字。如果你把它们都写成sum一个参数是MatrixXd、另一个是const MatrixXd那么调用sum(A)时两个版本都算匹配编译器直接报错call of overloaded sum(Eigen::MatrixXd) is ambiguous。想对比性能就写两个名字或者分两次编译。34.7 常见编译错误速查报错原因解决No such file: Eigen/Dense头文件路径没设对加-I指向 Eigen 目录尺寸不匹配固定矩阵Matrix2d * Matrix3d这类写法本身就不成立编译期报static assertion failed: INVALID_MATRIX_PRODUCT改行列数尺寸不匹配动态矩阵MatrixXd的行列数要到运行期才知道Debug 运行期断言、Release静默给你错数见第 26 章 26.3中文乱码编码问题g UTF-8MSVC/utf-8no match for operator*Matrix/Array 混用用.array()/.matrix()统一固定矩阵 resize 报错用了Matrix3d却 resize换MatrixXd三角视图不能 cout分解返回视图加.toDenseMatrix()34.8 调试技巧下面每一小段都是独立的检查手段抄用时请确保上文已经把对应的m、llt、A、x、b定义好llt见第 30 章A、b见第 29 章// 打印中间结果std::coutdebug: m \nmstd::endl;// 检查分解是否成功if(llt.info()!Eigen::Success){std::cout分解失败std::endl;}// 检查求解残差doubleerr(A*x-b).norm();std::cout残差 errstd::endl;// 应接近 0// 检查有没有 NaN / Inf数值算崩了第一时间看这个std::cout含 NaN? m.hasNaN()std::endl;// 一个 boolstd::cout全都有限? (m.array().isFinite()).all()std::endl;// 逐元素后再 .all()hasNaN()和isFinite()的分工实测——m取1 2 / NaN 4h取1 2 / 3 Inf写法返回什么m含 NaNh含 Infm.hasNaN()一个booltruefalsem.array().isFinite()每个元素一个bool打印出来是一整块true true / false truetrue true / true false(m.array().isFinite()).all()一个boolfalsefalse两个要点①isFinite()是逐元素的想要整体是否有限必须再.all()第 28 章的归约②hasNaN()认不出Inf——矩阵里出现inf时它照样返回false所以查数值异常要认准isFinite()。实测用一个奇异矩阵A 1 2 / 2 4解A*x b得到x -inf infhasNaN()说没问题而(x.array().isFinite()).all()正确地返回false。34.9 本章完整示例一个程序把本章的每个坑都走一遍正确写法存成ch34.cpp按附录 C.3 编译#includeEigen/Dense#includeiostream// ✅ 函数参数用常量引用不复制整个矩阵34.6doublesumOf(constEigen::MatrixXdm){returnm.sum();}intmain(){// 1) 矩阵乘法Eigen 自动用临时变量左右同源也安全Eigen::Matrix2d a;a1,2,3,4;aa*a;std::couta a * a 之后:\na\n;// 2) 转置自赋值必须 transposeInPlace() 或 .eval()34.1 第二档Eigen::MatrixXdt(2,3);t1,2,3,4,5,6;t.transposeInPlace();std::coutt 原地转置后3x2:\nt\n;// 3) 单操作数的逐元素自运算实测安全不需要 .eval()Eigen::ArrayXdv(3);v1,2,3;vv.square();std::coutv 逐元素平方后 v.transpose()\n;// 4) 右边带 transpose 且与左边同源整块 .eval() 才是对的34.1 第三档Eigen::Matrix2d w;w1,2,3,4;w(w.array()*w.transpose().array()).eval();std::coutw (w.array() * w.transpose().array()).eval():\nw\n;// 5) 已知不重叠时用 noalias()省掉临时矩阵Eigen::MatrixXd AEigen::MatrixXd::Ones(2,2);Eigen::MatrixXd BEigen::MatrixXd::Constant(2,2,3.0);Eigen::MatrixXdC(2,2);C.noalias()A*B;std::coutC A * Bnoalias:\nC\n;// 6) auto 捕获的是表达式不是结果Eigen::Matrix2d m;m1,2,3,4;autolazym.transpose();m5,6,7,8;// 改了 mstd::cout改 m 之后再打印 lazy它跟着变了:\nlazy\n;// 7) 想要当下这份数用 .eval() 固化autosnapshotm.transpose().eval();m.setZero();std::coutsnapshot 不受 m 影响:\nsnapshot\n;std::cout按引用求和 sumOf(m) sumOf(Eigen::MatrixXd(m))\n;return0;}运行结果a a * a 之后: 7 10 15 22 t 原地转置后3x2: 1 4 2 5 3 6 v 逐元素平方后 1 4 9 w (w.array() * w.transpose().array()).eval(): 1 6 6 16 C A * Bnoalias: 6 6 6 6 改 m 之后再打印 lazy它跟着变了: 5 7 6 8 snapshot 不受 m 影响: 5 7 6 8 按引用求和 sumOf(m) 0最能说明auto陷阱的是第 6、7 两段lazy打印出来是改动之后的m的转置5 7 / 6 8而snapshot是当时算好的实体之后m.setZero()也动不了它。最后一行sumOf(m) 0也顺带说明m被清零了按引用传进去读到的就是最新状态。动手做两个对照实验这比读十遍规则有用把第 4 段的.eval()删掉写成w w.array() * w.transpose().array();再跑。实测它连 Debug 都不报错直接给你1 12 / 6 16——正确值是1 6 / 6 16。这就是第三档静默算错的样子。把第 3 段改成v v.square().eval();再跑输出完全不变还是1 4 9。单操作数的逐元素自运算本来就安全.eval()在这里只是多余的拷贝。顺带把第 2 段改成t t.transpose();Debug 构建会当场断言中止第二档而 Release 构建不吭声地把t算成错的值。同一个错误写法Debug 和 Release 表现不同——这正是只在 Release 测过一遍不可靠的原因。34.10 小结别名分三档矩阵乘法自动安全单操作数的逐元素自运算实测安全右边带transpose()的自赋值最危险——Debug 断言、Release 静默算错必须.eval()或用InPlace。断言只在 Debug 生效别只在 Release 里验证正确性。noalias()跳过临时变量。别用auto捕获矩阵表达式。开-O2优化是性能第一要务实测-O0慢约 37 倍-O1就已够-O3相对-O2无额外收益。传参用const MatrixXd。下一章学习Eigen 进阶技巧——与 STL 结合、常用模式、更多坑。练习题写出安全的原地转置写法。写出w w.array() * w.transpose().array();的安全版本并说明原写法在 Debug 和 Release 下分别是什么表现。用.noalias()优化一个矩阵乘法赋值。演示 auto 陷阱auto 捕获表达式后改原矩阵观察结果变化。说说为什么开编译优化对 Eigen 这么重要。