這次實作主要是熟悉一些 stl 使用,語法盡可能使用 C++,挑用兩種不同資料結構,結果感到有些意外。
第一種資料結構是單純用指標配置一維 heap,做 index 轉換 ( 就是數值分析習慣用的一維模擬二維) ;
另一種資料結構採用 vector< vararray<T> >。先把意外的結論寫下來。
valarray 在 vc 底下之操作並沒顯得優勢 ,反之所費時間約為一般陣列存取之兩倍;而 g++ 下之 valarray 確實有操作上之優勢。
另外大陸的行(row)列(column),與台灣的行(column)列(row) 在中文上是相反的,感到困惑網友請注意這點。
[1] 原理篇
這裡用的求反矩陣,必須有一份暫存矩陣,而不是原地做。高斯消去法求反矩陣不熟請參考其他相關資料,這裡講的是一些小細節注意地方。假設欲求矩陣 src 之反矩 dst,一開始 dst 先設單位矩陣。在簡化過程之第 k 個迭代時,
a. 列交換( row swap, Rij)
若要將 src 第 i 列 / 第 j 列互換時,由於第 i 列與第 j 列前面 k 個元素都一定被消成 0 了,所以做列互換時只需行第 k 個元素交換即可;但 dst 一定要所有元素都經過交換。
b. 行交換 (column swap, Cij)
將 src 第 i 行 / 第 j 行互換時,這沒話說,不論是 src 還是 dst 必將所有元素全都交換
c. 第 i 列乘以 v 加到第 j 列 (Rij (v) )
和列交換相仿,src 由於前面 k 個元素必被消成 0 ,所以做這動作時,只需從第 k 個元素開始執行即可;而 dst 則必須所有元素都進行這步驟。
[2] 程式碼與執行結果
這份程式碼頗長,由於是拿來做測時比較,故上述的列交換、Rij(v) 等都沒經過化簡,程式碼如下。
- #include <iostream>
- #include<algorithm>
- #include <cmath>
- #include <vector>
- #include <valarray>
- #include <ctime>
- using namespace std;
- template<typename T>
- bool minv1(const T * src, T * dst, const size_t n, const T eps)
- {
- const size_t n2=n*n;
- size_t i, j, k, is, js;
- T pivot, tmp;
- T * mbk = new T [n2];
- copy(src, src+n2, mbk); // copy src to mbk
- fill(dst, dst+n2, 0); // set dst to unit matrix
- for(i=0; i<n; ++i) dst[i*n+i]=1;
- for(k=0; k<n; ++k){ // execute k times
- pivot = 0; // find pivot
- for(i=k; i<n; ++i) for(j=k; j<n; ++j)
- if(pivot < (tmp = abs(mbk[i*n+k])))
- pivot=tmp, is=i, js=j;
- if(pivot<=eps){ // has no solution
- delete [] mbk;
- return false;
- }
- if(is!=k){ // swap row(is,k)
- swap_ranges(mbk+k*n, mbk+k*n+n, mbk+is*n);
- swap_ranges(dst+k*n, dst+k*n+n, dst+is*n);
- }
- if(js!=k){ // swap col(js,k)
- for(i=0; i<n; ++i){
- swap(mbk[i*n+k], mbk[i*n+js]);
- swap(dst[i*n+k], dst[i*n+js]);
- }
- }
- pivot = mbk[k*n+k]; // adjust pivot to 1
- for(j=0; j<n; ++j){
- mbk[k*n+j]/=pivot;
- dst[k*n+j]/=pivot;
- }
- // elimination
- for(i=0; i<n; ++i){
- if(i!=k){
- tmp = -mbk[i*n+k];
- for(j=0; j<n; ++j){
- mbk[i*n+j]+=tmp*mbk[k*n+j];
- dst[i*n+j]+=tmp*dst[k*n+j];
- }
- }
- }
- }
- delete mbk;
- return true;
- }
- template<typename T>
- bool minv2(vector< valarray<T> > src, vector<valarray<T>> & dst, const size_t n, const T eps)
- {
- const size_t n2=n*n;
- size_t i, j, k, is, js;
- T pivot, tmp;
- valarray<T> row_tmp(n);
- for(i=0; i<n; ++i) dst[i][i]=1;
- for(k=0; k<n; ++k){ // execute k times
- pivot = 0; // find pivot
- for(i=k; i<n; ++i) for(j=k; j<n; ++j)
- if(pivot < (tmp = abs(src[i][k])))
- pivot=tmp, is=i, js=j;
- if(pivot<=eps) // has no solution
- return false;
- if(is!=k){ // swap row(is,k)
- row_tmp = src[is], src[is] = src[k], src[k]=row_tmp;
- row_tmp = dst[is], dst[is] = dst[k], dst[k]=row_tmp;
- }
- if(js!=k){ // swap col(js,k)
- for(i=0; i<n; ++i){
- swap(src[i][k], src[i][js]);
- swap(dst[i][k], dst[i][js]);
- }
- }
- pivot = src[k][k]; // adjust pivot to 1
- src[k]/=pivot, dst[k]/=pivot;
- // elimination
- for(i=0; i<n; ++i){
- if(i!=k){
- tmp = -src[i][k];
- src[i]+=tmp*src[k];
- dst[i]+=tmp*dst[k];
- }
- }
- }
- return true;
- }
- template<typename T>
- void gen(T *& arr, const size_t n){
- const size_t n2=n*n;
- for(size_t i=0; i<n2; ++i)
- arr[i] = rand() % 10;
- }
- int main()
- {
- const size_t n = 1000;
- const double eps=1e-9;
- double *x = new double[n*n];
- double *y = new double[n*n];
- vector< valarray<double> > vva(n, valarray<double>(n));
- vector< valarray<double> > vvb(n, valarray<double>(n));
- clock_t ck;
- gen(x,n);
- for(size_t i=0; i<n; ++i) for(size_t j=0 ; j<n; ++j)
- vva[i][j] = x[i*n+j];
- ck = clock();
- minv1(x, y, n, eps);
- cout << "minv1 : " << clock()-ck << endl;
- ck = clock();
- minv2(vva, vvb, n,eps);
- cout << "minv2 : " << clock()-ck << endl;
- delete [] x;
- delete [] y;
- system("pause");
- return 0;
- }
上面要做輸出還是幹嘛的自己再加,這裡不附。
vc2010 , release 預設參數(含O2) 執行結果如下
minv1 : 7328
minv2 : 14276
Code::Blocks 10.5 (gcc4.4.1) ,-O2,執行結果如下
minv1 : 8423
minv2 : 7143
針對這結果其實蠻意外的,valarray 在 vc 底下之操作並沒顯得優勢 ,反之所費時間約為一般陣列存取之兩倍(其實網路上找得到有人針對 vc 下之 valarray 存取做處理,但速度也只與 pointer + heap 相仿);而 g++ 下之 valarray 確實有操作上之優勢。
假設變數是
double tmp = 2.0;
valarray<double> a(10), b(10);
但 valarray 很尷尬的是,我們只能寫成
b = tmp * sin(a);
這樣上述的精簡技巧就完全不能用,因實際要做的可能只會是
b[2:9] = tmp * sin(a [2:9] )
至於迴圈精簡後的程式碼,下一份供參考。
- template<typename T>
- bool minv(const T* src, T* dst, const size_t n, const T eps)
- {
- const size_t n2=n*n;
- size_t i, j, k, is;
- T pivot, tmp;
- T * mbk = new T [n2];
- copy(src, src+n2, mbk); // copy src to mbk
- fill(dst, dst+n2, 0); // set dst to unit matrix
- for(i=0; i<n; ++i) dst[i*n+i]=1;
- for(k=0; k<n; ++k){ // execute k times
- pivot= abs(mbk[k*n+k]), is=k;
- for(i=k+1; i<n; ++i) // find pivot
- if(pivot < (tmp = abs(mbk[i*n+k])))
- pivot=tmp, is=i;
- if(pivot<=eps){ // has no solution
- delete [] mbk;
- return false;
- }
- if(is!=k){ // swap row(is,k)
- swap_ranges(mbk+k*n+k, mbk+k*n+n, mbk+is*n+k);
- swap_ranges(dst+k*n, dst+k*n+n, dst+is*n);
- }
- pivot = mbk[k*n+k]; // adjust pivot to 1
- for(j=k; j<n; ++j) mbk[k*n+j]/=pivot;
- for(j=0; j<n; ++j) dst[k*n+j]/=pivot;
- // elimination
- for(i=0; i<n; ++i){
- if(i!=k){
- tmp = -mbk[i*n+k];
- for(j=0; j<n; ++j) dst[i*n+j]+=tmp*dst[k*n+j];
- for(j=k; j<n; ++j) mbk[i*n+j]+=tmp*mbk[k*n+j];
- }
- }
- }
- delete mbk;
- return true;
- }
一些更細節的問題,像是回圈合併,這個就不再做撰碼討論。

我覺得你沒有完全理解 valarray 背後的概念。批次操作固然是 valarray 優化的機會之一,valarray 另一個加速的空間在於用 slicing 篩選操作對象,這表示有時候你只需要改變索引對應的方式,而不需要真正去搬移資料,就可以輕鬆達到行、列交換的目的。 你提的這個例子,只要幫 slice_array overload 幾個函數就做得到 b[2:9] = tmp * sin(a [2:9] ) 現在 C++ 的 valarray 有一點雞肋的感覺,界面並不好用,普通使用者懶得學(例如本人),而真正需要數值處理的人會跑去裝專用程式庫,所以制定標準和實作STL的那些人也比較不關注 valarray。 ps. 我只是說你還沒接觸到 valarray 的典型用法而已,並不代表 valarray 典型用法就一定比你這裡的程式還好,我不知道結果如何。
C++ valarray 典型用法,我可能也真的摸不到 (除非哪天真的派我用 C++ re-develop,但這機率應該和中樂透差不多),不過有時間還是會補起來的 (我好像說了好多次這句話...),提昇效能(前提是維護性不能降太多)的議題一直讓我感到興趣。 最後感謝您的留言,受益匪淺。
說老實話我也不太會用 valarray,只會基本的 slice 而已,而且在可預見的未來我也沒打算花時間弄熟,沒用到的東西就算不會也沒什麼大不了的。知識和技術本來就是隨著實作經驗與時俱進。 一個人的專注力有限,應該放在最能創造價值的事物上,這是一個 Explore vs. Exploit 的兩難。
Explore / Exploit 真的是兩難!!