newしすぎてうざい


 スレッドからスレッドに要求投げるんですよ。パケットサイズもたまーに可変な奴があって。
 呼び出し側スタックにパケットの実体を置いて、スレッド越しにポインタだけ投げていいなら一番コスト安いんだけどさ。受け取られるまでは送信側でスタック変数を生かしておく必要がありますね。論外ですね。
 んじゃ静的記憶だと、えーと、可変長のリクエストだから…。どうすんだ?

 ってとこで考えるのをやめてnewしまくった。しかもスレッド越しに渡すからshared_ptrで包んだ。リクエスト一個一個で毎回毎回shared_ptr<packet_base> packet_sp ( new packet_derived( arg1, arg2 ) )みたいな。さすがにイラッと来るものが。
 まあでも、リクエストの粒度が粗ければ大したことも無いか、とか思うようにしてたんだけどさ。割と細かく出来ないと厳しい気がしてきたんですよ。あんまり細かすぎてもスレッド増やす意味無くなるけど。つーか元々意味無いんじゃねーの(笑)。いや練習という大事な意味が(笑)。

 ということで真面目に考えた。
 …引数の受け渡し用のパイプを何本か作ればいいだけじゃね?
 一対一のパイプだからpush/popの順番が狂ったりも無いし。むしろ引数を遅延して渡せるのは便利な気もするな。メモリ効率はどうなんだろう。まーPC向けだから気にするレベルじゃないな。つーかnewしまくるよりはなあ。
 よーし早速書いてみよう。いや、やっぱ明日くらいにしよう(笑)。


素直にメモリバリア


 つーか別に普通に_ReadWriteBarrier()置いてasm吐かせて中見れば安心じゃん、と思ったのでそうした。asm出力変化なし。めでたしめでたし。
 というわけで、前のエントリのコードのとこにも_ReadWriteBarrier()を入れといた。

追記
 色々あってRMBとかWMBとかで誤魔化した。


超はやめろ


 ソースがでかくなってきたから、スレッド間の受け渡し処理を整理しようと書き換えたら、何だか大体一秒ごとにカクッとするようになった。
 超意味わかんないしー超ムカツクーとか思って色々見てるうちに超うざくなってきて、放り出して書き直してやりたいっつーかマルチスレッドとか何で書いてんだよ俺ーみたいな超投げやりな気持ちになってきたが、ふと気付くと単に初期化処理の要求パケットを間違って一秒ごとに投げてたことに気付いたのであることだなあ。


普通のコードがどうコンパイルされるのかチラ見してみた


 どうコンパイルされるのか分かってねーとCとかC++とかは書きにくい気が、などとぬかしつつも現実には「詳細はよくわかんねーけど最適化してくれるはずだよね」って部分が9割だよな俺の場合、とも思ってた。
 で、実際に吐かれるコードをちゃんと見た記憶が遠くなりつつある今、volatile周辺の挙動も気になったので、かなーり久々にアセンブラ出力させてみた。

 とりあえず、問題のスレッド間通信用リングバッファのコードは、

template < typename T, size_t Size >
class t2t_pipe : noncopyable {
  T buf_[Size];
  volatile size_t head_, tail_;

  // リングバッファの次インデックスを取得する関数 //
  size_t next_index_of ( size_t index ) const {
    size_t next = index + 1;
    if ( next == Size ) {
      return 0;
    }
    return next;
  }

public:
  t2t_pipe () : head_(0), tail_(0) {
    assert( Size >= 2 );
    WMB(); // VC++2008でx86でvolatileだから記述不要?
  }

  bool empty () const {
    RMB(); // これも(これ以下も)全部不要?
    return head_ == tail_;
  }

  bool full () const {
    RMB();
    return head_ == next_index_of( tail_ );
  }

  T pop () {
    assert( ! empty() ); // 呼び出し側は先に必ずempty()見ろよ的設計
    T result;
    RMB();
    result.swap( buf_[head_] );
    head_ = next_index_of( head_ );
    WMB();
    return result;
  }

  void push ( const T& item ) {
    assert( ! full() );
    RMB();
    buf_[tail_] = item;
    tail_ = next_index_of( tail_ );
    WMB();
  }
};

こんな感じ。大丈夫なんだろうかこれ。特にbuf_がvolatileじゃない点とか。インデックス操作のメモリバリアさえ万全なら、関数が操作対象とする要素はvolatileな挙動をしていないはずだから、大丈夫そうな気はするけどいまいち自信が。つーか本当にどっか根本的に間違ってそうで不安だ。
 まあでもその辺は置いといて、実際にt2t_pipe< shared_ptr<foo_base_class>, 16384 > foo_pipe;にpop()を掛けてるところのコンパイル結果はこんな感じになっていた。

_TEXT SEGMENT
何たら$shared_ptr@何たら@@@boost@@どーのこーの PROC
  mov DWORD PTR [eax], 0
  mov DWORD PTR [eax+4], 0
  mov ecx, DWORD PTR 何たら$shared_ptr@何たら+131072
  lea ecx, DWORD PTR 何たら$shared_ptr@何たら[ecx*8]
  push esi
  cmp eax, ecx
  je SHORT $LN9@pop
  mov esi, DWORD PTR [ecx]
  mov edx, DWORD PTR [eax]
  mov DWORD PTR [eax], esi
  mov DWORD PTR [ecx], edx
$LN9@pop:
  mov edx, DWORD PTR [ecx+4]
  mov esi, DWORD PTR [eax+4]
  mov DWORD PTR [ecx+4], esi
  mov DWORD PTR [eax+4], edx
  mov ecx, DWORD PTR 何たら$shared_ptr@何たら+131072
  lea edx, DWORD PTR [ecx+1]
  mov ecx, edx
  sub ecx, 16384
  neg ecx
  sbb ecx, ecx
  and ecx, edx
  mov DWORD PTR 何たら$shared_ptr@何たら+131072, ecx
  pop esi
  ret 0
何たら$shared_ptr@何たら@@@boost@@どーのこーの ENDP
_TEXT ENDS

 うーん。期待以上のとこと以下のとこがあるなあ…。

 とりあえず、いきなりeaxを使ってる辺りfastcall系っぽくて呼び出し規約がまず分からないんだが、最後にreturnしてるからインラインではないよな。0を書き込んでる辺り、T result;の処理かな。その後でswapっぽいこともしてるしな。つーか全くアトミックに見えないんだけど大丈夫なのか(笑)。shared_ptrそのもののコピー関連はスレッドセーフらしいけど。ポイントした先の操作は普通にスレッドセーフじゃないけど。
 メモリバリア周辺での異常行動は無いように見えるっつーか、その辺を見たいならempty()の待機ループとかも見ないと意味無いか。えーと見てきました。大丈夫に見えました。一見。
 つーかぶっちゃけ前半よく分かんねえ(笑)。呼び出し規約が分からないのと、shared_ptrの実装が分からないのとで。
 next_index_of()がインライン展開された後半の辺りは、subしてnegしてsbbしてand、という手法で分岐を抑制していた。確か例の電卓で全く同じ最適化を無駄にやってた気がする。
 だが、こいつはテンプレートクラスで、Sizeはコンパイル時点で16384の定数だから、後半部分に本当に期待していたのはandのマスクだったんだが。
 まあ、人間の手でこう書けばいいんだよな…。

  size_t next_index_of ( size_t index ) const {
    size_t next = index + 1;
    if ( is_power_of_2( Size ) ) {
      return next & Size-1;
    }
    if ( next == Size ) {
      return 0;
    }
    return next;
  }

 つーかこれ、is_power_of_2()のとこがインライン展開で定数化されないと逆に遅いよな。どうなるんだろう。うまく行ったとしても、条件式が定数だよとか、死んでるコードパスがあるよとか、余計な警告も出そうだよな。SFINAEを利用したりすればどうにかなるのだろうか。テンプレートは初歩的な使い方しか理解してないからさっぱり分からんが。
 で、どうでもいいところに不要な最適化を書くな、って話になるんだが(笑)。分かってます。大丈夫です。今捨てますから。コンストラクタにassert(is_power_of_2(Size))とかも書きませんから。…いや別にそっちでいい気もするな。つーかstatic_assert早くくれ。Boostのは例によってIntellisenseが死ぬし。
 メモリに厳しい世界なら頑張り甲斐もあるんだろうなあ。でもすぐ飽きるんだろうなあ俺(笑)。

 まあ、よほどのところ以外はコンパイラ任せで大体平気っぽいよな、とは思いました。
 そんなことより他に考えることが山ほどあるよな、とも思いました。とりあえずnewしまくりは大丈夫なんだろうかとか。でも最近はアロケータも結構賢いらしいけど。まー駄目そうなら「設計失敗しましたー、次行ってみよう」でいいか(笑)。別に今回のは遅くても困らないのに、無駄にマルチスレッドにして遊んでるようなもんだし(笑)。
 派生クラスのインスタンスをnewして、ベースクラス型のshared_ptrに突っ込んでスレッドまたいで渡してる辺りなんかも、どんなコードが生成されてるのか理解してないんだよなあ。ちゃんと派生クラスのdeleteが掛かる仕様らしいけど。

 つーか、Express Editionだとプロファイラが無いのがなあ…。MS謹製のスタンドアロン版は試してみようとしてめんどくさくて気力が尽きたし。タダだから文句は言えないが、こんなとこでケチらなくてもって気はしなくもなく。