[Eigen中文文档] 深入了解 Eigen - 惰性求值与混叠(Aliasing)

简介: Eigen具有智能的编译时机制,可以实现惰性求值并在适当的情况下删除临时变量。它会自动处理大多数情况下的混叠问题,例如矩阵乘积。自动行为可以通过使用MatrixBase::eval()和MatrixBase::noalias()方法手动覆盖。

文档总目录

英文原文(Lazy Evaluation and Aliasing)

执行摘要:Eigen具有智能的编译时机制,可以实现惰性求值并在适当的情况下删除临时变量。它会自动处理大多数情况下的混叠问题,例如矩阵乘积。自动行为可以通过使用MatrixBase::eval()MatrixBase::noalias()方法手动覆盖。

当你编写涉及复杂表达式的代码时,例如:

mat1 = mat2 + mat3 * (mat4 + mat5);

Eigen会自动为每个子表达式确定是否将其计算为临时变量。确实,在某些情况下,将子表达式计算为临时变量更好,而在其他情况下则最好避免这样做。

没有表达式模板的传统数学库总是将所有子表达式计算为临时变量。因此,在以下代码中,

vec1 = vec2 + vec3;

传统的库会将vec2 + vec3计算为一个临时变量vec4,然后将vec4复制到vec1中。这显然是低效的:数组被遍历两次,有很多无用的加载/存储操作。

基于表达式模板的库可以避免将子表达式计算为临时变量,这在许多情况下会使速度显著提高。这被称为延迟/惰性求值,因为表达式会尽可能晚地进行求值。在Eigen中,所有表达式都是延迟求值的。更准确地说,一旦表达式分配给矩阵,它便开始被求值。在此之前,除了构建抽象表达式树外,什么也不会发生。然而,与大多数其他基于表达式模板的库不同,Eigen可能会选择将某些子表达式计算为临时变量。这有两个原因:首先,纯延迟求值并不总是性能的最佳选择;其次,纯延迟求值可能会非常危险,例如矩阵乘法:如果将mat = mat * mat直接在目标矩阵中求值矩阵乘积,则会得到错误的结果,因为矩阵乘积的方式不同。

出于这些原因,Eigen具有智能的编译时机制,可以自动确定哪些子表达式应计算为临时变量。如下示例中:

mat1 = mat2 + mat3;

Eigen选择不引入任何临时变量。因此,数组只被遍历一次,产生了优化的代码。如果你真的想强制立即求值,可以使用eval()函数,如下:

mat1 = (mat2 + mat3).eval();

如下是一个更复杂的示例:

mat1 = -mat2 + mat3 + 5 * mat4;

在这个例子中,Eigen也不会引入任何临时变量,从而产生一个单一的融合计算循环,这显然是正确的选择。

哪些子表达式将被计算为临时变量?

默认的计算策略是将操作融合到一个循环中,除了少数情况下,Eigen默认选择该策略。

第一种情况

Eigen选择计算子表达式的第一种情况是当它看到一个赋值操作a = b;,且表达式b带有 在赋值之前计算 的标志,最重要的这样的表达式是矩阵乘积表达式。例如,当执行以下操作时:

mat = mat * mat;

Eigen会将mat * mat计算成一个临时矩阵,然后将其复制到原始的mat中。这可以保证正确的结果,因为我们之前看到,惰性计算在矩阵乘积中会产生错误的结果。这也不会花费太多的计算代价,因为矩阵乘积本身的代价要高得多。请注意,这个临时矩阵仅在计算时引入,也就是在此示例中的=中。表达式mat * mat仍然返回一个抽象的乘积类型。

如果你明确计算结果不会与乘积的操作发生混叠现象,并想强制使用惰性计算,则可以使用.noalias()。下面是一个例子:

mat1.noalias() = mat2 * mat2;

在这里,由于我们知道mat2不是与mat1相同的矩阵,因此我们知道惰性求值不会有危险,可以强制使用惰性求值。具体来说,noalias()的作用是绕过evaluate-before-assigning标志。

第二种情况

Eigen选择评估子表达式的第二种情况是当它看到嵌套表达式,例如a + b时,其中b已经是一个具有evaluate-before-nesting标志的表达式。同样,这类表达式中最重要的例子是矩阵乘积表达式。例如,当执行以下操作时:

mat1 = mat2 * mat3 + mat4 * mat5;

在这个例子中,矩阵乘积mat2 * mat3mat4 * mat5会被分别求值为临时矩阵,然后在mat1中进行求和。实际上,为了有效地计算矩阵乘积,需要在手头有一个目标矩阵内进行求值,而不是像简单的 点积 那样。然而,对于小矩阵,你可能希望使用lazyProduct()来强制执行基于 点积 的惰性求值。再次强调,重要的是要理解,这些临时矩阵仅在计算时创建,即在operator=中。请参见TopicPitfalls_auto_keyword了解与此说明相关的常见陷阱。

第三种情况

第三种情况是,当Eigen的成本模型显示,如果将子表达式求值为临时变量,操作的总成本将会减少时,Eigen会选择求值该子表达式。实际上,在某些情况下,如果一个中间结果的计算成本足够高,而且被重复使用的次数足够多,则该中间结果值得被 “缓存”。以下是一个例子:

mat1 = mat2 * (mat3 + mat4);

在这里,假设矩阵至少有2行2列,表达式mat3 + mat4的每个系数在矩阵乘积中会被使用多次。与每次计算总和相比,一次计算并将其存储在一个临时变量中要好得多。Eigen理解这一点,并在求值乘积之前将mat3 + mat4求值为一个临时变量。

相关文章
|
存储 算法 NoSQL
[Eigen中文文档] 存储顺序
矩阵和二维数组有两种不同的存储顺序:列优先和行优先。本节解释了这些存储顺序以及如何指定应该使用哪一种。
165 0
|
C++
[Eigen中文文档] 按值将Eigen对象传递给函数
对于 Eigen,这一点更为重要:按值传递固定大小的可向量化 Eigen 对象不仅效率低下,而且可能是非法的或使程序崩溃! 原因是这些 Eigen 对象具有对齐修饰符,在按值传递时会不遵守这些修饰符。
165 0
|
存储 算法 NoSQL
[Eigen中文文档] 稀疏矩阵操作
在许多应用中(例如,有限元方法),通常要处理非常大的矩阵,其中只有少数系数不为零。在这种情况下,可以通过使用仅存储非零系数的特殊表示来减少内存消耗并提高性能。这样的矩阵称为稀疏矩阵。
463 0
|
存储 编译器
|
存储 C语言 C++
|
存储 安全 编译器
[Eigen中文文档] 常见的陷阱
本文将介绍一些Eigen常见的陷阱
268 0
|
存储 索引
[Eigen中文文档] 扩展/自定义Eigen(三)
本页面针对非常高级的用户,他们不害怕处理一些Eigen的内部细节。在大多数情况下,可以通过使用自定义一元或二元函数避免使用自定义表达式,而极其复杂的矩阵操作可以通过零元函数(nullary-expressions)来实现,如前一页所述。 本页面通过示例介绍了如何在Eigen中实现新的轻量级表达式类型。它由三个部分组成:表达式类型本身、包含有关表达式编译时信息的特性类和评估器类,用于将表达式评估为矩阵。
137 1
|
并行计算 算法 安全
[Eigen中文文档] Eigen 和多线程
某些 Eigen 算法可以利用硬件中存在的多个内核。
439 0
|
存储 编译器 调度
[Eigen中文文档] 固定大小的可向量化Eigen对象
本文主要解释 固定大小可向量化 的含义。
112 0