ikautak.log

C/C++, Python, CUDA, Android, Linux kernel, Network, etc.

More Effective C++ 項目17 遅延評価の使用を検討する

新訂版 More Effective C++ (AddisonーWesley professional co)

新訂版 More Effective C++ (AddisonーWesley professional co)

  • 作者: スコット・メイヤーズ,安村通晃,伊賀聡一郎,飯田朱美,永田周一
  • 出版社/メーカー: ピアソンエデュケーション
  • 発売日: 2007/06/29
  • メディア: 単行本(ソフトカバー)
  • 購入: 8人 クリック: 129回
  • この商品を含むブログ (43件) を見る

項目17 遅延評価の使用を検討する

計算結果が本当に必要になるまで計算を遅らせる、遅延評価が有効な例。

参照回数計測

次のコードを考える。

class String { ... };

String s1 = "Hello";
String s2 = s1;      // Stringコピーコンストラクタを呼ぶ

s2をs1で初期化したとき、s2も"Hello"のコピーを作るのが「積極評価」。

s2を初期化するときは"Hello"をs1と共有し、s2から参照していること覚えておく。
そしてs2に修正が入ったとき、初めてコピーを作るのが「遅延評価」。
本当に必要になるまで何かのコピーをするな、ということ。
Linuxのfork()も、子プロセスのメモリに書き込みがあるまで親プロセスとメモリを共有しているし、コピーオンライトを使えということか。
遅延評価を使った参照回数計測は項目29。

読み出しと書き込みを区別する
String s = "Hello";
...
cout << s[3];  // s[3]を読むためにoperator[]を呼ぶ
s[3] = 'x';    // s[3]に書くためにoperator[]を呼ぶ

読むのは簡単だが、書き込みで新しいコピーを作るのは難しい実装が必要。
operator[]の内部でreadとwriteを区別する必要があるが、項目30で出てくるproxyクラスを使うと出来るらしい。

遅延フェッチ

多くのフィールドからなる大きなオブジェクトを使用するプログラムを考える。

class LargeObject {             // 大きな永続的オブジェクト
public:
  LargeObject(ObjectID id);     // オブジェクトをディスクから復元する

  const string& field1() const; // field1の値
  int field2() const;           // field2の値
  double field3() const;        // ...
  const string& field4() const;
  const string& field5() const;
  ...
};

ディスクからLargeObjectを復元させるコストを考える。

void restoreAndProcessObject(ObjectID id) {
  LargeObject object(id);  // オブジェクトを復元させる
  ...
}

LargeObjectのインスタンスは巨大なので、全てのデータを取得するのはコストのかかる処理とする。
例えば、次のアプリケーションの場合、

void restoreAndProcessObject(ObjectID id) {
  LargeObject object(id);
  if (object.field2() == 0) {  // field2だけ使う
    cout << "Object " << id << ": null field2.\n";
  }
}

ここではfield2だけが必要なので、他のフィールドを取得するのは無駄となる。

そこで、LargeObjectが作られたときはディスクからデータを読まずに、オブジェクトの殻だけを作り、必要になった時だけ、ディスクからデータを読むようにする。

class LargeObject {
public:
  LargeObject(ObjectID id);

  const string& field1() const;
  int field2() const;
  double field3() const;
  ...

private:
  ObjctId oid;
  mutable string* field1Value;  // mutableを付ける
  mutable int* filed2Value;
  mutable double *field3Value;
  ...
};

LargeObject::LargeObject(ObjectID id)  // フィールドをnullで初期化
: oid(id), field1Value(0), field2Value(0), field3Value(0), ...
{}

const string& LargeObject::field1() const {
  if (field1Value == 0) {
    // field1をまだ読んでなければ、ディスクから
    // field1を読みだしてfield1Valueに入れる
    ...
  }
  return field1Value;
}

オブジェクトの各フィールドをnullポインタで初期化する。
各メンバ関数はそれが指し示すデータにアクセスする前に、ポインタの状態をチェックし、nullならばディスクから読んでくる。
このような遅延フェッチを実装すると、const関数であるgetter内でポインタを修正する必要があるので、各フィールドにはmutableが必要となる。

式の遅延評価

最後の例は数値計算

template<class T>
class Matrix { ... };        // 同型な行列

Matrix<int> m1(1000, 1000);  // 1000x1000の行列
Matrix<int> m2(1000, 1000);

Matrix m3 = m1 + m2;         // m1とm2の加算

通常、operator+の実装は積極評価を用いるが、この行列計算の場合は大量の計算が必要となり、メモリのコストもかかる。 よって、m3の値はm1とm2の和である、というデータだけをm3に保存する。
m1とm2のポインタ、加算であるとことを示すenumなどを保持する。

m3を作った限りは必ず計算結果を使うのではと思ったが、行列は特定の要素だけ参照するケースが多く、全部計算する必要はほとんどなりらしい。
1960年代に開発されたプログラミング言語APLは、この遅延評価で、当時のマシンでも行列演算をインタラクティブにやってのけたそうだ。

まとめ

遅延評価は不必要なオブジェクトのコピーや数値計算を避けることができるが、本当に全ての計算が必要な場合には、遅延評価向けデータの更新などで処理が増えるため、速度が遅くなり、メモリも消費する。
遅延評価を使っているかどうかは、クラスのインターフェースには現れないので、後から積極評価に変えたり、プロファイラを使ってボトルネック部分だけ遅延評価に変えたりすることが可能である。