どうコンパイルされるのか分かってねーと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謹製のスタンドアロン版は試してみようとしてめんどくさくて気力が尽きたし。タダだから文句は言えないが、こんなとこでケチらなくてもって気はしなくもなく。