ETはもともと,線形代数ライブラリの構築において発案されたものであり,今回の私の例もこの例を取り上げます.
まず,Vectorクラスの構築を考えてみます.Vectorといってもstd::vectorのような可変長配列ではなくて線形代数で言うところのVectorなので注意してください.さしあたって以下のような和とスカラー倍の機能を提供した実装が考えられます.
#includeclass Vector{ size_t const n; double * val; public: Vector(size_t n, double v = double()) : n(n) , val(new double[n]) { for(size_t i = 0; i < n; ++i){ val[i] = v; } } Vector(Vector const & rhs) : n(rhs.n) , val( new double[n] ) { for(size_t i = 0; i < n; ++i){ val[i] = rhs.val[i]; } } ~Vector() { delete[] val; } Vector & operator=(Vector const & rhs) { assert(this->dim() == rhs.dim()); for(size_t i = 0; i < n; ++i){ val[i] = rhs.val[i]; } return *this; } double operator[](size_t i) const { return val[i]; } double & operator[](size_t i) { return val[i]; } size_t dim() const { return n; } Vector & operator+=(Vector const & rhs) { assert(this->dim() == rhs.dim()); for(size_t i = 0; i < n; ++i){ val[i] += rhs.val[i]; } return *this; } Vector & operator*=(double rhs) { for(size_t i = 0; i < n; ++i){ val[i] *= rhs; } return *this; } Vector operator+(Vector const & rhs) const { assert(this->dim() == rhs.dim()); Vector tmp(*this); tmp += rhs; return tmp; } Vector operator*(double rhs) const { Vector tmp(*this); tmp *= rhs; return tmp; } friend Vector operator*(double lhs, Vector const & rhs) { Vector tmp(rhs); tmp *= lhs; return tmp; } };
(上のコードは例を示すために超適当にでっちあげたものなのであんまり厳しいツッコミは勘弁してくださいw.)
そして,上のVectorクラスを用いる場合,例えば以下のようにして使うはずです.
Vector u(1000), v(1000), w(1000), t(1000); // some process... t = u + v + w;
ここで問題となるのは,u + v + wという式が一時オブジェクトを生じるということです.つまり,上のコードは以下と同等になります.
Vector u(1000), v(1000), w(1000), t(1000); // some process... Vector tmp(u); tmp += v; Vector tmp2(tmp); tmp2 += w; t = tmp2;
つまり,t = u + v + wという書き方はsizeof(double) * 2000バイトのメモリと3000回のdoubleのcopyを暗に必要とします.しかし,理想的には
for(size_t i = 0; i < u.dim(); ++i){ t[i] = u[i]; t[i] += v[i]; t[i] += w[i]; }
と書けば,余分なメモリは不要であり,コピーも必要最低限の回数で済むはずです.
このように演算子によって巨大な一時オブジェクトと大量のコピーが発生しうるというのはC++特有の問題で,詳しい議論などは「Effective C++」などに譲ります.
では,常に後者のように書けば良いじゃないか,というのが最も単純な解ですが,後者の書き方しか許されないとすると,コード量が増大しコードが非常に読みにくくなってしまいます.(これはバグが入り込む可能性を増大させることにもつながります)
あるいは,もう一つ以下のように書けば良いという指摘も考えられます.
t = u; t += v; t += w;
この書き方なら,先の効率的なコードと同等ではあります.しかし,このような書き方でもまだ直感的ではなく読みにくいという問題があります.さらには,op=の形でもoverheadを生じざるを得ない状況があります.
w = 1.2 * u + 1.5 * v;
このコードをop=の形で書いたとしても
Vector tmp(u); tmp *= 1.2; w = v; w *= 1.5; w += tmp;
と書かざるを得ず,一時オブジェクトとコピーのoverheadが避けられません.しかし,この例でも理想的には
for(size_t i = 0; i < u.dim(); ++i){ w[i] = 1.2 * u[i]; w[i] += 1.5 * v[i]; }
のように一時オブジェクトとコピーのoverheadを最小限にすることができるはずです.
現在のハードウェアアーキテクチャでは,メモリアクセスが最大のボトルネックであり,メモリにアクセスする量が少ないほど速いというのが多くの状況下で真です.このような状況での巨大な一時オブジェクトと大量のコピーの発生は,処理速度の致命的な低下につながります.
このような「コードの読みやすさと効率の間のジレンマ」がC++における数値計算ライブラリが抱えていた問題であり,この問題に対して素晴らしい解決策を提供してくれたのがETです.
それでは時間が出来れば,次はETの基本的な考え方と簡単な実装を書きたいと思います.
id:Cryolite:20040507#p1へ続く.