鉴于上述规范,哪个选项最有效?
如您所料,最有可能的选项 1。当然,这取决于更新需要什么。所以你可能需要一些基准测试。但总的来说,与分配新对象所允许的次要优化相比,分配成本是显着的。此外,选项 2 会产生复制结果的额外费用。
是否保证使用 X 段调用 Foo3 始终至少与使用相同段调用 Foo4 一样高效?
如果您调用Foo4(x.segment(...)),它会分配一个新向量并将段复制到其中。这比 Foo3 贵得多更多。您唯一获得的是矢量将正确对齐。这对现代 CPU 来说只是一个很小的好处。所以我希望 Foo3 更有效率。
请注意,您没有考虑过一个选项:使用模板。
template<class Derived>
void Foo1(const Eigen::MatrixBase<Derived>& x) {
Eigen::MatrixBase<Derived>& mutable_x = const_cast<Eigen::MatrixBase<Derived>&>(x);
// Update mutable_x.
}
const-cast 令人讨厌但无害。请参阅 Eigen 关于该主题的文档。
https://eigen.tuxfamily.org/dox/TopicFunctionTakingEigenTypes.html
总的来说,这将允许与内联函数体大致相同的性能。但是,在您的特定情况下,它可能不会比 Foo1 的内联版本快。这是因为一般段和 Ref 对象具有基本相同的性能。
访问 Ref 与 Vector 的效率
让我们更详细地了解在 Eigen::Vector、Eigen::Ref<Vector>、Eigen::Matrix 和 Eigen::Ref<Matrix> 上的计算之间的性能。 Eigen::Block(Vector.segment() 或 Matrix.block() 的返回类型)在功能上与 Ref 相同,所以我不再赘述。
- Vector 和 Matrix 保证整个数组与 16 字节边界对齐。这允许操作使用对齐的内存访问(例如,在本例中为
movapd)。
- Ref 不保证对齐,因此需要未对齐的访问(例如
movupd)。在非常旧的 CPU 上,这曾经会导致显着的性能损失。如今,它的相关性降低了。对齐很好,但它不再是矢量化的全部。引用 Agner 关于该主题的内容 [1]:
当访问跨缓存线边界的未对齐数据时,某些微处理器会损失几个时钟周期。
大多数没有 VEX 前缀的读取或写入 16 字节内存操作数的 XMM 指令要求操作数按 16 对齐。接受未对齐的 16 字节操作数的指令在旧处理器上可能效率很低。但是,AVX 和更高版本的指令集大大缓解了这种限制。 AVX 指令不需要内存操作数的对齐,除了显式对齐的指令。支持的处理器
AVX 指令集通常非常有效地处理未对齐的内存操作数。
- 所有四种数据类型都保证内部维度(向量中的唯一维度,矩阵中的单列)连续存储。所以 Eigen 可以沿着这个维度向量化
- Ref 不保证沿外部维度的元素是连续存储的。从一列到下一列可能存在间隙。这意味着像
Matrix+Matrix 或Matrix*Scalar 这样的标量操作可以对所有行和列中的所有元素使用单个循环,而Ref+Ref 需要一个嵌套循环,其中一个外循环覆盖所有列,一个内循环覆盖所有行。
- Ref 和 Matrix 都不保证特定列的正确对齐。因此,大多数矩阵运算(例如矩阵向量乘积)都需要使用非对齐访问。
- 如果在函数内创建向量或矩阵,这可能有助于转义和别名分析。但是,Eigen 在大多数情况下已经假定没有别名,并且 Eigen 创建的代码几乎没有给编译器添加任何内容的空间。因此,它很少有好处。
- 调用约定存在差异。例如在
Foo(Eigen::Ref<Vector>) 中,对象是按值传递的。 Ref 有一个指针、一个大小,并且没有析构函数。所以它将在两个寄存器中传递。这是非常有效的。对于消耗 4 个寄存器(指针、行、列、外部步幅)的 Ref<Matrix> 而言,它不太好。 Foo(const Eigen::Ref<const Vector>&) 将在堆栈上创建一个临时对象并将指针传递给函数。 Vector Foo() 返回一个具有析构函数的对象。所以调用者在堆栈上分配空间,然后将隐藏指针传递给函数。通常,这些差异并不显着,但它们当然存在,并且可能与通过许多函数调用进行很少计算的代码相关
考虑到这些差异,让我们看看手头的具体案例。你还没有指定更新方法的作用,所以我必须做一些假设。
计算总是相同的,所以我们只需要查看内存分配和访问。
示例 1:
void Foo1(Eigen::Ref<Eigen::VectorXd> x) {
x = Eigen::VectorXd::LinSpaced(x.size(), 0., 1.);
}
Eigen::VectorXd Foo2(int n) {
return Eigen::VectorXd::LinSpaced(n, 0., 1.);
}
x.segment(..., n) = Foo2(n);
Foo1 执行一次未对齐的内存写入。 Foo2 将一次分配和一次对齐的内存写入临时向量。然后它复制到段。这将使用一个对齐的内存读取和一个未对齐的内存写入。因此 Foo1 显然在所有情况下都更好。
示例 2:
void Foo3(Eigen::Ref<Eigen::VectorXd> x)
{
x = x * x.maxCoeff();
}
Eigen::VectorXd Foo4(const Eigen::Ref<Eigen::VectorXd>& x)
{
return x * x.maxCoeff();
}
Eigen::VectorXd Foo5(const Eigen::Ref<Eigen::VectorXd>& x)
{
Eigen::VectorXd rtrn = x;
rtrn = rtrn * rtrn.maxCoeff();
return rtrn;
}
Foo3 和 4 都从 x 读取两次未对齐的内存(一次用于 maxCoeff,一次用于乘法)。之后,它们的行为与 Foo1 和 2 相同。因此 Foo3 总是优于 4。
Foo5 为初始副本执行一次未对齐的内存读取和一次对齐的内存写入,然后为计算执行两次对齐的读取和一次对齐的写入。之后跟随函数外部的副本(与 Foo2 相同)。这仍然比 Foo3 所做的要多得多,但是如果您对向量进行更多的内存访问,那么在某些时候它可能是值得的。我对此表示怀疑,但可能存在案例。
主要内容是:由于您最终希望将结果存储在现有向量的段中,因此您永远无法完全避免未对齐的内存访问。所以不用太担心他们。
模板与参考
差异的简要说明:
模板版本(如果编写得当)适用于所有数据类型和所有内存布局。例如,如果你传递一个完整的向量或矩阵,它可以利用对齐。
在某些情况下 Ref 根本无法编译,或者工作方式与预期不同。如上所述,Ref 保证内部维度是连续存储的。调用 Foo1(Matrix.row(1)) 将不起作用,因为矩阵行没有连续存储在 Eigen 中。如果你用const Eigen::Ref<const Vector>& 调用一个函数,Eigen 会将该行复制到一个临时向量中。
模板化版本可以在这些情况下工作,但它当然不能矢量化。
Ref 版本有一些好处:
- 阅读更清晰,意外输入出错的机会更少
- 您可以将它放在一个 cpp 文件中,这样可以减少冗余代码。根据您的用例,更紧凑的代码可能更有益或更合适
[1]https://www.agner.org/optimize/optimizing_assembly.pdf