为何按列分步计算Schur补的效率低于整体矩阵乘法实现?
为什么按列拆分的Schur补计算反而比直接矩阵乘法更慢?
你观察到的这个现象其实挺有意思——本来以为避开大型矩阵乘法的按列求和方案会更高效,但实际在Eigen和x86处理器上反而更慢。咱们从几个核心角度拆解一下背后的原因:
1. Eigen的矩阵乘法是高度优化的“黑科技”
Eigen的operator*和矩阵转置操作背后是经过极致打磨的实现:
- SIMD向量化充分利用:Eigen会自动适配Intel处理器的AVX/SSE指令集,一次能处理多个浮点运算。方案1里的
W*V_inv.asDiagonal()*W.transpose(),Eigen可以把整个运算链(对角矩阵缩放+矩阵乘法)合并优化,让内存访问和计算逻辑完全对齐SIMD的要求,把CPU的运算单元榨满。 - 缓存友好的分块策略:矩阵乘法(GEMM)的实现会用分块技术,把大矩阵拆成适配CPU缓存的小块,大幅减少缓存miss。而方案2的逐列循环每次只取W的一列,反复对
delta_H做秩1更新,这种零散的内存访问很难利用缓存优势,反而会触发更多的缓存失效。
2. 逐列循环的低效细节拉低了性能
看你方案2的代码:
for (int i = 0; i < V.cols(); ++i) { double V_inv = 1.0 / V(i, i); double V_inv_sqrt = std::sqrt(V_inv); Eigen::VectorXd M = W.block(0, i, W.rows(), 1) * V_inv_sqrt; Eigen::VectorXd M1 = W.block(0, i, W.rows(), 1) * V_inv; delta_H += M * M.transpose(); delta_b += M1 * b_b[i]; }
这里有几个明显的性能瓶颈:
- 重复内存读取:每次循环都要读取W的同一列两次(生成M和M1),白白浪费了内存带宽。
- 秩1更新的计算密度极低:
M * M.transpose()本质是把单个向量的元素广播到矩阵中,大部分时间都在做内存写入,而非真正的浮点运算。相比之下,方案1的矩阵乘法是计算密集型任务,CPU的浮点单元能被充分利用。 - 循环与临时对象的额外开销:每次循环里创建临时向量
M、M1,即使Eigen用了栈分配,也会有额外的初始化、拷贝开销;循环本身的分支判断、计数也会累积消耗,当V的列数较多时,这些开销会被显著放大。
3. 对角矩阵乘法的特殊优化加持
方案1里的V_inv.asDiagonal(),Eigen会自动识别这是对角矩阵,在和W相乘时做特殊优化——本质就是把W的每一列乘以对应的V_inv元素,这个操作可以完全向量化,而且不需要额外存储中间结果。后续和W.transpose()的矩阵乘法,Eigen会把整个运算链融合成高度优化的代码,没有中间步骤的内存浪费。
4. 处理器架构放大了差距
不管是Core i7还是Atom,方案1更快的趋势一致,Atom上差距更明显的原因是:
- Atom的缓存容量更小,方案2的逐列操作会导致更频繁的缓存miss,性能下降更严重。
- Core i7的AVX指令集宽度更大(256位),方案1的向量化优化能发挥更大优势;而方案2的逐列操作很难利用宽SIMD指令,只能做单精度/双精度的 scalar 运算。
给你的优化小建议
如果想保留逐列思路的同时提升性能,可以试试这些调整:
- 复用W的列数据:一次读取W的列,计算好
V_inv和V_inv_sqrt的缩放值,避免重复读取内存。 - 用Eigen的
rankUpdate函数:Eigen提供了MatrixBase::rankUpdate方法,专门优化A*A.transpose()这类秩1更新,比手动实现快很多,因为它内部做了向量化处理。比如把delta_H += M * M.transpose();改成delta_H.rankUpdate(M);。 - 合并临时操作:直接在W的列上做缩放后调用
rankUpdate,减少临时向量的创建和拷贝。
优化后的方案2代码片段示例:
Eigen::VectorXd V_inv(V.cols()); for (int jack = 0; jack < V.cols(); ++jack) { V_inv[jack] = 1.0/V(jack, jack); } Eigen::MatrixXd scaled_W = W * V_inv.cwiseSqrt().asDiagonal(); delta_H.rankUpdate(scaled_W); // 等价于逐列秩1更新,但效率更高
内容的提问来源于stack exchange,提问作者jhuai
相关产品推荐
相关产品推荐

