簡介
別名混亂Aliasing
是指在賦值表達式中,一個Eigen對象(矩陣、數組、向量)同時出現在左值和右值表達式中,比如v = v*2; m = m.transpose();
;
別名混亂會引起錯誤,從而產生問題,比如m = m.transpose();
這裏將介紹什麼是別名混亂,如何來避免發生錯誤的情況。
aliasing 示例
先給一個示例,從例子中來獲得直觀印象。本示例期望將左上角的2X2的塊覆蓋右下角的塊。
//matrix_aliasing1.cpp
#include <iostream>
#include <Eigen/Dense>
using namespace std;
using namespace Eigen;
int main()
{
MatrixXi mat(3,3);
mat << 1, 2, 3, 4, 5, 6, 7, 8, 9;
cout << "Here is the matrix mat:\n" << mat << endl;
// This assignment shows the aliasing problem
mat.bottomRightCorner(2,2) = mat.topLeftCorner(2,2);
cout << "After the assignment, mat = \n" << mat << endl;
}
執行的結果如下:
$ g++ -I /usr/local/include/eigen3 matrix_aliasing1.cpp -o matrix_aliasing1
promote:eigen david$
promote:eigen david$ ./matrix_aliasing1
Here is the matrix mat:
1 2 3
4 5 6
7 8 9
After the assignment, mat =
1 2 3
4 1 2
7 4 1
從結果可以看到,實際的結果並不如我們鎖期望。很明顯,mat.bottomRightCorner(2,2) = mat.topLeftCorner(2,2);
的寫法有問題。
係數mat(1,1)在bottomRightCorner(2,2) , topLeftCorner(2,2)
都存在,在賦值後,右下角的塊的係數(2,2)將保存爲原mat(1,1)的值5。但實際其值爲1。問題是Eigen使用的是lazy evaluation
延遲求值。所以,操作的過程類似:
mat(1,1) = mat(0,0);
mat(1,2) = mat(0,1);
mat(2,1) = mat(1,0);
mat(2,2) = mat(1,1);
於是,mat(2,2) 被賦予的新值最終來自於mat(0,0), mat(1,1) <-- mat(0,0)
,所以結果不是預期的樣子。
在收縮(shrink)一個矩陣時,更容易有代碼產生別名混亂。就比如:表達式語句 vec = vec.head(n) ; mat = mat.block(i,j,r,c).
都產生錯誤。
比較麻煩的是,別名混亂aliasing 在編譯時,編譯器並不能識別處理。然而,Eigen可以在執行時偵測到某些別名混亂。
下面的示例,
//
Matrix2i a;
a << 1, 2, 3, 4;
cout << "Here is the matrix a:\n" << a << endl;
a = a.transpose(); // !!! do NOT do this !!!
cout << "and the result of the aliasing effect:\n" << a << endl;
如果編譯運行,執行的效果應該如下(但需要多一點操作,請看下面):
Here is the matrix a:
1 2
3 4
and the result of the aliasing effect:
1 2
2 4
當然,我們知道了這有別名混亂的問題,結果也確實如此。 但Eigen在執行時,使用了運行時斷言,給出了提升消息。如此這樣:
void Eigen::DenseBase<Derived>::checkTransposeAliasing(const OtherDerived&) const
[with OtherDerived = Eigen::Transpose<Eigen::Matrix<int, 2, 2, 0, 2, 2> >, Derived = Eigen::Matrix<int, 2, 2, 0, 2, 2>]:
Assertion `(!internal::check_transpose_aliasing_selector<Scalar,internal::blas_traits<Derived>::IsTransposed,OtherDerived>::run(internal::extract_data(derived()), other))
&& "aliasing detected during transposition, use transposeInPlace() or evaluate the rhs into a temporary using .eval()"' failed.
提示: 要關閉這樣的斷言,請定義一個宏:EIGEN_NO_DEBUG
,並編譯程序。然後你就可以編譯運行,得到這樣的錯誤結果。
解決別名混亂
只有知道了別名混亂的根源,那麼就可以解決問題了。方法就是,對右值就行計算求值,並賦予臨時變量,再爲左值賦值,就可以解決問題了。對右值的求值,使用函數.eval()
完成。
Eigen提供了示例:
MatrixXi mat(3,3);
mat << 1, 2, 3, 4, 5, 6, 7, 8, 9;
cout << "Here is the matrix mat:\n" << mat << endl;
// The eval() solves the aliasing problem
mat.bottomRightCorner(2,2) = mat.topLeftCorner(2,2).eval();
cout << "After the assignment, mat = \n" << mat << endl;
現在,執行可以得到正確的結果:
Here is the matrix mat:
1 2 3
4 5 6
7 8 9
After the assignment, mat =
1 2 3
4 1 2
7 4 5
上面問題示例中的a = a.transpose();
會產生問題,簡單地修改爲a = a.transpose().eval();
就可以正確了。但更通用,更好的方式是:使用Eigen提供的專用函數來操作 – Eigen提供了transposeInPlace()
函數可以完成這個任務。
MatrixXf a(2,3); a << 1, 2, 3, 4, 5, 6;
cout << "Here is the initial matrix a:\n" << a << endl;
a.transposeInPlace();
cout << "and after being transposed:\n" << a << endl;
執行結果也是正確的:
Here is the initial matrix a:
1 2 3
4 5 6
and after being transposed:
1 4
2 5
3 6
只有一個操作有對應的Eigen函數xxxInPlace()
,那就放心地使用,其更高效,更正確。下面列表列出了Eigen提供的對應函數:
原函數 | In-place函數 |
---|---|
MatrixBase::adjoint() | MatrixBase::adjointInPlace() |
DenseBase::reverse() | DenseBase::reverseInPlace() |
LDLT::solve() | LDLT::solveInPlace() |
LLT::solve() | LLT::solveInPlace() |
TriangularView::solve() | TriangularView::solveInPlace() |
DenseBase::transpose() | DenseBase::transposeInPlace() |
其他的情況:
在上面提到的a = a.head(n)
,可以使用函數conservativeResize()
來執行操作。
別名混亂與面向組件
的操作
如上所述,如果矩陣或者數組對象同時在賦值表達式的左右兩邊,這是就可能發生錯誤。但可以通過在右邊顯式調用求值函數eval()
來改進。
然而,採樣面向組件的操作,比如矩陣相加,乘以標量,或者數組乘法,這些都是安全的。
下面示例只有面向組件的操作,它們是安全的,所有無需eval()
求值的顯式調用。
MatrixXf mat(2,2);
mat << 1, 2, 4, 7;
cout << "Here is the matrix mat:\n" << mat << endl << endl;
mat = 2 * mat;
cout << "After 'mat = 2 * mat', mat = \n" << mat << endl << endl;
mat = mat - MatrixXf::Identity(2,2);
cout << "After the subtraction, it becomes\n" << mat << endl << endl;
ArrayXXf arr = mat;
arr = arr.square();
cout << "After squaring, it becomes\n" << arr << endl << endl;
// Combining all operations in one statement:
mat << 1, 2, 4, 7;
mat = (2 * mat - MatrixXf::Identity(2,2)).array().square();
cout << "Doing everything at once yields\n" << mat << endl << endl;
執行結果如下:
Here is the matrix mat:
1 2
4 7
After 'mat = 2 * mat', mat =
2 4
8 14
After the subtraction, it becomes
1 4
8 13
After squaring, it becomes
1 16
64 169
Doing everything at once yields
1 16
64 169
別名混亂與矩陣乘法
缺省狀態下,在Eigen內,在目標矩陣不調整尺寸大小
時,矩陣乘法是唯一一個假定有別名混亂的操作。所以,如果matA是一個方陣,那麼matA = matA * matA;
計算就是安全的。所有其它的操作都認爲是安全的,沒有別名混亂的問題,要麼是因爲計算結果賦予了一個不同的矩陣對象,要麼使用了面向組件
的操作函數。
如下有簡單示例:
MatrixXf matA(2,2);
matA << 2, 0, 0, 2;
matA = matA * matA;
cout << matA;
執行結果:
4 0
0 4
然而,這也帶來了一定的代價。比如matA = matA * matA;
運算,這裏的矩陣乘法表達式,會賦予一個臨時變量對象,然後計算結果再賦予matA
對象。而賦值表達式matB = matA * matA;
也會一樣多使用一個臨時變量。這種情況下,更有效率的方式是直接計算乘積,然後賦值給matB
對象,而不多用一個臨時變量。
這就需要使用noalias()
函數來完成工作,其告知這裏沒有別名混亂。使用方式如此matB.noalias() = matA * matA;
。
示例:
MatrixXf matA(2,2), matB(2,2);
matA << 2, 0, 0, 2;
// 這是使用了臨時變量週轉的計算賦值模式。
matB = matA * matA;
cout << matB << endl << endl;
// 這是比較高效的模式。
matB.noalias() = matA * matA;
cout << matB;
執行結果:
4 0
0 4
4 0
0 4
**注意:**從Eigen3.3開始,如果乘法計算時,目標矩陣的尺寸大小會發生變化時,也認爲不會有別名混亂的錯誤。這時,你需要自己處理。
MatrixXf A(2,2), B(3,2);
B << 2, 0, 0, 3, 1, 1;
A << 2, 0, 0, -2;
A = (B * A).cwiseAbs();
cout << A;
這裏執行(3X2)矩陣被(2X2)矩陣乘,執行結果得到(3X2)矩陣。矩陣A從(2X2)–> (3X2):
4 0
0 6
2 2
但這裏依然有錯。正確的實現方法應該如下:
MatrixXf A(2,2), B(3,2);
B << 2, 0, 0, 3, 1, 1;
A << 2, 0, 0, -2;
A = (B * A).eval().cwiseAbs();
cout << A;
注意這裏的A = (B * A).eval().cwiseAbs();
,顯式調用了eval()
。
總結
別名混亂髮生於矩陣或數組對象在賦值操作時同時位於左右兩邊,對同樣的多個係數進行操作了。
- 但在面向係數計算的別名混亂是無害的,這包括標量乘於矩陣或數組,矩陣或數組的加法。
- 而在2個矩陣相乘時,Eigen假定會發生別名混亂。如果我們確定這裏並沒有發生別名混亂的錯誤,我們可以使用
noalias()
。 - 剩下的其他情況下,Eigen假定沒有別名混亂的問題,但如果,此時真實情況是有別名混亂的問題發生的,則會導致錯誤發生。爲防止此情況發生,必須調用
eval()
或者xxxInPlace()
函數。