コンパイラの吐くコード
C++プログラマーたるもの(少なくともライブラリ・プログラマーは)、コンパイラがどんなコードを吐くのか、どういうコードが最適化されやすいのかを知っておく必要があると思います。
自分はよく最適化に関して疑問に思ったコード片を(狭義の)コンパイルして、アセンブリソースを比較します。
作業ディレクトリには、そうしたコード片がたくさん残っています。
今日は、そのディレクトリに新たに追加された2つのコード片を紹介します。
コンパイル時の文字列生成
tmp.write("\x03\x00\x01", 3);
writeの引数の最初の2バイトは拡張ヘッダの長さ、3バイト目は拡張ヘッダの種別です。
通常は4バイト目以降にヘッダの内容が続くのですが、ここでは空です。
このハードコーディングされた文字列を呼び出しコードから削除して、
write_empty_header(tmp, 0x01);
と書ければ格段に読みやすくなります。
(当然、0x01には列挙値や定数に置き換えることができますが、ここでは考えないことにします。)
write_empty_headerの単純な実装はこうです。
template<class Sink> void write_empty_header(Sink& sink, unsigned char type) { const char data[] = { '\x03', '\x00', static_cast<char>(type) }; sink.write(data, 3); }
果たして、このコードは最初の例と同じコードになるのか?
より一般化して、コンパイル時の文字列生成と実行時の文字列初期化の分岐点はどこなのか?
実験してみました。
// 文字列を捨てないためのダミーの関数 void bar(const char*); // ケース0: 単純呼び出し void foo0() { bar("hoge"); } // ケース1: 定数の引数で文字配列を初期化 inline void hoge1(char c1, char c2, char c3, char c4) { const char buf[5] = { c1, c2, c3, c4, '\0' }; bar(buf); } void foo1() { hoge1('h', 'o', 'g', 'e'); } // ケース2: テンプレート引数で文字配列を初期化 template<char C1, char C2, char C3, char C4> inline void hoge2() { static const char buf[5] = { C1, C2, C3, C4, '\0' }; bar(buf); } void foo2() { hoge2<'h','o','g','e'>(); } // ケース3: テンプレートのメンバを使う template<char C1, char C2, char C3, char C4> struct hoge_traits { static const char buf[5]; }; template<char C1, char C2, char C3, char C4> const char hoge_traits<C1,C2,C3,C4>::buf[5] = { C1, C2, C3, C4, '\0' }; template<char C1, char C2, char C3, char C4> inline void hoge3() { bar(hoge_traits<C1,C2,C3,C4>::buf); } void foo3() { hoge3<'h','o','g','e'>(); }
結果は、VC++8.0、g++3.4.4、CodeWarrior8.3の全てでケース1だけ配列への代入が発生することになりました。
今回の場合、効率を落とさないためにはヘッダの種別をテンプレートパラメータに移すしかなく、
template<unsigned char Type, class Sink> void write_empty_header(Sink& sink) { const char data[] = { '\x03', '\x00', static_cast<char>(Type) }; sink.write(data, 3); } write_empty_header<0x01>(tmp);
のようになります。
ラッパークラスの除去
同じくLZHのヘッダ処理から、
// MS-DOSファイル属性値を16ビット符号なし整数(リトルエンディアン)で出力する iostreams::write_uint16<little>(tmp, header_.attributes);
これを、
// ラッパークラス struct uint_little16 { explicit uint_little16(boost::uint16_t x) : value(x) {} boost::uint16_t value; }; // バイナリ出力用定義(ここでは本質的な問題ではない) template<> struct struct_traits<uint_little16> { typedef boost::mpl::list< member<uint_little16, boost::uint16_t, &uint_little16::value, little> > members; }; iostreams::binary_write(tmp, uint_little16(header_.attributes));
に変更することを考えます。
何やら無駄に複雑になっている感じですが、こうすることで整数型を構造体と同じようbinary_writeで扱うことができる利点が生まれます。
uint_little16はエンディアンを区別するためだけのラッパークラスです。
このラッパークラスは最適化の結果消えるのか?
ということで実験です。
// ラッパークラス struct hoge { explicit hoge(int n) : n(n) {} int n; }; // ダミー関数 void bar(int n); // ケース0: 単純呼び出し void foo0(int n) { bar(n); } // ケース1: ラッパーを生成してその場で使う void foo1(int n) { bar(hoge(n).n); } // ケース2: ラッパーを生成して、関数に渡す inline void foo2_impl(const hoge& h) { bar(h.n); } void foo2(int n) { foo2_impl(hoge(n)); }
結果はこうなりました。
コンパイラ | 結果 |
---|---|
VC++8.0 | 全て同じ |
g++3.4.4 | ケース1、2ではラッパー生成にコストがかかる |
g++4.1.1 | 全て同じ |
CodeWarrior8.3 | ケース1、2ではラッパー生成にコストがかかる |
CodeWarriorはともかく、意外にもg++3.4.4ではラッパー生成のコストが残る結果となりました。追試したg++4.1.1でようやく最適化されました。
少し前のコンパイラでは、このようなごく薄いラッパーでもコストがかかるということは覚えておいたほうが良いでしょう。
自分はこの結果ならガンガン使いますけどね。