Chiharu の日記

絵描き C/C++ プログラマーの日記です。

Parallel force - パラレル・フォース 〜スレッド周り、ひとまず実装終わりです

ロック機構の実装に手間取りました。
これまでロック機構というと、クライアント コードは下記のように実装してきていたのですが…、

CriticalSection aCs;
{
    Lock<CriticalSection> aLock(aCs);
    // このブロック中はロックされる
}

このうち、

Lock<CriticalSection> aLock(aCs);

のように毎度ロック用の型を明示するのが面倒くさくて、パラレル・フォースでは

auto aLock = lock(aCs);

のように記述できるよう lock メソッドの実装を進めていました。せっかく左辺値の型推論が有効なのですから、型名は極力省略したいものです。
さて、この lock メソッド実装の課題は 2 つ。

  • Lock<> はコピー不能を前提とする。
  • Lock<> のコンストラクタ以上の大きなオーバーヘッドを発生させない。

メソッドの型は下記を想定します。

template <class LockObject>
Lock<LockObject> lock(LockObject& iLockObject);

C++0x より前の枠組みで素直に実装すると、下記のようになってしまいます。

template <class LockObject>
Lock<LockObject> lock(LockObject& iLockObject)
{
    return Lock<LockObject>(iLockObject);
}

これでは Lock<> インスタンスのコピーが 1 度以上発生してしまいます。RVO を期待したところで、それはコンパイラの最適化ヒントにつき、言語仕様上、上記のコードでコピーを抑制することはできません。
コピー抑制について、C++0x を知る前の自分では、戻り値を std::auto_ptr > として Lock<> を new してしまうことくらいしか、アイデアがありませんでした。負けた気分です。new 演算子を使用する方法では、前者は満足しますが、後者のパフォーマンスに問題が発生します。直感的に、高々ロック処理程度で new 演算子をコールしたくはないと感じます。ロック対象がクリティカルセクションならまだ良いかもしれませんが、たとえば速度重視でスピンロックする場合などは、new 演算子によるパフォーマンス劣化は許容できるものではなくなってしまいます。
いろいろ調べた結果、よりよい解決方法は C++0x の右辺値参照にありました。
右辺値参照の詳細は他のサイトや書籍に譲りますが、概要としては std::auto_ptr のような破壊的セマンティクスを C++ の文法レベルで導入したものです。lock メソッドにおいては、Lock<> にムーブ コンストラクタとムーブ演算子を実装した上で、下記のように実装しました。

template <class LockObject>
Lock<LockObject> lock(LockObject& iLockObject)
{
    Lock<LockObject> aLock(iLockObject);
    return std::move(aLock);
}

これで、Lock<> インスタンスのコピーは抑制され、コンストラクタ以上のオーバーヘッドは、ムーブ コンストラクタ & ムーブ演算子に記載のポインタコピー程度となり、非常に満足いく結果が得られました。

CriticalSection aCs;
{
    auto aLock = lock(aCs);
    // このブロック中はロックされる
}

その後、スレッド関連オブジェクトのロックだけでなく、イメージの画素ロックにも Lock<> クラスを使用するようになり、部分特殊化したテンプレートクラスに画素データへのアクセサを用意した都合上、lock メソッドはもう少し変化していますが、おおむね上記のような感じで収まっています。クライアントコードには変化はなく、当初の目的だった、クライアントコードを極力省略することそれ自体はきちんと達成されています。
んー。C++0x 便利。素敵過ぎます。