为何Eigen中张量连续切片视图的运算比原张量更快?
测试背景
我针对Eigen张量的三种场景做了元素级乘法(tensor = tensor * tensor)的性能测试,对比耗时:
- 原始2D张量(3000x3000)
- 从同尺寸张量切出的连续切片视图(切片范围覆盖整个张量)
- 从更大3D张量中切出的非连续切片视图
测试代码
#include <iostream> #include <Eigen/Dense> #include <unsupported/Eigen/CXX11/Tensor> #include <chrono> #include <numeric> #include <vector> #include <tuple> #include <iomanip> using namespace Eigen; using namespace std; using namespace std::chrono; template <typename T> double performTestAndMeasureTime(T& tensor) { auto start = high_resolution_clock::now(); tensor = tensor * tensor; auto end = high_resolution_clock::now(); duration<double, std::milli> elapsed = end - start; return elapsed.count(); } int main() { cout << std::fixed << std::setprecision(16); const int size = 3000; const int numTests = 10; for (int j = 0; j < 10; ++j) { Tensor<double, 2> tensor(size, size); tensor.setConstant(j); Tensor<double, 2> tensor_(size, size); tensor_.setConstant(j); auto tensorView1 = tensor_.slice(Eigen::array<Index, 2>{0, 0}, Eigen::array<Index, 2>{size, size}); Tensor<double, 3> largerTensor(100, size * 2, size * 2); auto tensorView2_ = largerTensor.chip(50, 0); auto tensorView2 = tensorView2_.slice(Eigen::array<Index, 2>{0, 0}, Eigen::array<Index, 2>{size, size}); tensorView2.setConstant(j); vector<tuple<double, double, double>> results; for (int i = 0; i < numTests; ++i) { double res1 = performTestAndMeasureTime(tensor); double res2 = performTestAndMeasureTime(tensorView1); double res3 = performTestAndMeasureTime(tensorView2); results.emplace_back(res1, res2, res3); } double sum1 = 0, sum2 = 0, sum3 = 0; for (const auto& [res1, res2, res3] : results) { sum1 += res1; sum2 += res2; sum3 += res3; } cout << "Results for tensor constant = " << j << endl; cout << " Average Normal: " << sum1 / numTests << " ms" << endl; cout << " Average Sliced View: " << sum2 / numTests << " ms" << endl; cout << " Average Noncontiguous: " << sum3 / numTests << " ms" << endl; } return 0; }
测试输出
Results for tensor constant = 0 Average Normal: 109.6084899999999891 ms Average Sliced View: 86.6384500000000060 ms Average Noncontiguous: 559.3868599999999560 ms Results for tensor constant = 1 Average Normal: 107.4885000000000019 ms Average Sliced View: 86.0359700000000061 ms Average Noncontiguous: 562.5502300000000560 ms Results for tensor constant = 2 Average Normal: 106.0686999999999927 ms Average Sliced View: 85.6165200000000084 ms Average Noncontiguous: 539.1068299999999454 ms Results for tensor constant = 3 Average Normal: 105.9735300000000109 ms Average Sliced View: 85.8508899999999926 ms Average Noncontiguous: 554.8193299999999226 ms Results for tensor constant = 4 Average Normal: 112.9430100000000010 ms Average Sliced View: 86.7122600000000006 ms Average Noncontiguous: 559.5556199999999762 ms Results for tensor constant = 5 Average Normal: 107.4511799999999795 ms Average Sliced View: 87.0094900000000138 ms Average Noncontiguous: 552.7255600000000868 ms Results for tensor constant = 6 Average Normal: 115.6903399999999920 ms Average Sliced View: 90.0774900000000116 ms Average Noncontiguous: 590.9229000000000269 ms Results for tensor constant = 7 Average Normal: 110.9844900000000081 ms Average Sliced View: 87.2257299999999844 ms Average Noncontiguous: 563.0328299999999899 ms Results for tensor constant = 8 Average Normal: 108.2359100000000183 ms Average Sliced View: 85.5443699999999865 ms Average Noncontiguous: 555.7776999999999816 ms Results for tensor constant = 9 Average Normal: 107.4942699999999860 ms Average Sliced View: 84.7212200000000024 ms Average Noncontiguous: 544.9521099999999478 ms
编译信息
- 编译命令:
g++ -std=c++20 -I/usr/include/eigen3/ test.cpp -o tensor_test && ./tensor_test - Eigen版本:3.4.0
问题
非连续张量运算耗时更长是因为处理器缓存无法有效利用,这点很好理解。但我疑惑的是,为什么在Eigen中,对连续切片视图执行运算会比直接操作原张量快这么多?我原本以为切片视图需要先映射表达式再求值,速度应该更慢,想知道背后的原因。
回答
出现这种现象的核心原因在于Eigen对张量表达式的编译期优化策略,以及视图和原张量在内存布局、编译元信息上的细微差异:
表达式模板的优化差异
Eigen的张量模块依赖表达式模板延迟求值,避免临时对象。对于原张量Tensor<double,2>,赋值操作tensor = tensor * tensor会触发完整表达式展开,但编译器可能因原张量的"原生"类型身份,保留额外的边界检查或内存对齐逻辑;而连续切片视图SliceOp的类型信息更明确,编译器能精准推断内存访问模式,直接生成更紧凑的循环代码,省去不必要的分支或对齐判断。内存布局的隐式对齐差异
虽然原张量和连续切片视图的内存都是连续的,但SliceOp视图会继承底层张量的对齐属性,同时在编译期明确"无偏移、步长等于元素大小"的访问模式。而原张量的赋值操作涉及自身覆盖,编译器会插入额外的内存屏障或临时缓存逻辑防止写操作干扰读操作;切片视图的赋值目标是底层张量的连续区域,编译器可安全生成向量化读写指令,无需额外缓存保护。循环展开与向量化的效率差异
连续切片视图的内部迭代器可直接按最优块大小(适配CPU缓存行)展开循环,且视图类型信息明确了"连续无间隙"的内存访问,编译器能毫无顾虑地启用最高级向量化优化(如AVX2、AVX-512)。而原张量的迭代器因兼容通用张量操作,保留了可配置的步长逻辑,导致循环展开效率略低。测试中的缓存预热差异
每次测试循环都会重新创建张量和视图,连续切片视图的初始化更轻量,原张量构造可能涉及更多内存初始化逻辑,导致第一次测试的缓存预热效果不同。不过从多次测试平均值来看,这个因素影响较小,核心还是编译期优化的差异。
总结来说,Eigen对连续切片视图的表达式优化更激进,因为视图的类型信息明确了内存访问的连续性和无偏移特性,编译器能生成比原张量更高效的机器码,最终带来明显性能提升。
内容的提问来源于stack exchange,提问作者Yes

