Boost 1.40.0 の function

Boost 1.40.0 で Boost.Function が更新されました。
更新内容を見てみると、

  • Optimize the use of small target objects.
  • Make Boost.Function compile under BOOST_NO_EXCEPTIONS (#2499, #2494, #2469, #2466, #2900)
  • Various minor fixes (#2642, #2847, #2929 #3012)
Version 1.40.0

とあります。
2番目と3番目は興味が無いのでスルーですが、small target object を最適化したという更新は気になります。


small target object については以前書きました(function_allows_small_object_optimization - melpon日記 - HaskellもC++もまともに扱えないへたれのページ)。


で、どんな最適化したのかなと差分とって見てみたところ、なかなか面白いことになっています。
function_base の assign_to で

if (stored_vtable.assign_to(f, functor)) vtable = &stored_vtable.base;
else vtable = 0;

というコードが、以下のように変更されています。

if (stored_vtable.assign_to(f, functor)) {
  std::size_t value = reinterpret_cast<std::size_t>(&stored_vtable.base);
  if (boost::has_trivial_copy_constructor<Functor>::value &&
      boost::has_trivial_destructor<Functor>::value &&
      detail::function::function_allows_small_object_optimization<Functor>::value)
    value |= (std::size_t)0x01;
  vtable = reinterpret_cast<detail::function::vtable_base *>(value);
} else 
  vtable = 0;

なんか trivial なコピーコンストラクタとデストラクタを持っていて small target object な場合は(自前の)仮想テーブルのポインタの最下位ビットに 1 を立てています。
まあ大抵の環境では 4 バイト以上のアラインでしょうから問題ないとは思いますが、C++ の仕様的にはアウトなんじゃないかなーとか思ったり。ただこれは public でない部分なので、もしアウトな環境が出ても #ifdef で分岐とかすればいいだけですね。


でまあ、これがどういう風に使われているのか見てみると...

detail::function::vtable_base* get_vtable() const {
  return reinterpret_cast<detail::function::vtable_base*>(
           reinterpret_cast<std::size_t>(vtable) & ~(std::size_t)0x01);
}

bool has_trivial_copy_and_destroy() const {
  return reinterpret_cast<std::size_t>(vtable) & 0x01;
}

当然仮想テーブルを取得する際には最下位ビットのフラグは消してやる必要があるので、get_vtable() は最下位ビットを消して仮想テーブルを返します。
has_trivial_copy_and_destroy() はさっき立てたビットが立っているかどうかを返すようです。


あと function_base の assign_to_own あたりで

f.vtable->manager(f.functor, this->functor,
                          boost::detail::function::clone_functor_tag);

と書かれていた部分が以下のように変更されていて、

if (this->has_trivial_copy_and_destroy())
  this->functor = f.functor;
else
  get_vtable()->base.manager(f.functor, this->functor,
                               boost::detail::function::clone_functor_tag);

move_assign() で

f.vtable->manager(f.functor, this->functor,
                  boost::detail::function::move_functor_tag);

と書かれていた部分が以下のように変更されていて、

if (this->has_trivial_copy_and_destroy())
  this->functor = f.functor;
else
  get_vtable()->base.manager(f.functor, this->functor,
                           boost::detail::function::move_functor_tag);

clear() で

void clear()
{
  if (vtable) {
    reinterpret_cast<vtable_type*>(vtable)->clear(this->functor);
    vtable = 0;
  }
}

と書かれていた部分が以下のように変更されています。

void clear()
{
  if (vtable) {
    if (!this->has_trivial_copy_and_destroy())
      get_vtable()->clear(this->functor);
    vtable = 0;
  }
}

つまり「trivial なコピーコンストラクタとデストラクタを持っていて small target object な場合は、直接 assign したりデストラクタ呼ばなくてもいいじゃない」ってことらしいです。
確かに自前の仮想テーブルから呼び出すコストや引数をコピーするコストや呼び出し先で分岐してるコストを省いたりできるとは思いますが、そのために条件分岐を追加したりビット操作してるので、has_trivial_copy_and_destroy() の条件を満たさない場合には少し遅くなりそうです。
ただまあ、大抵の関数オブジェクトは has_trivial_copy_and_destroy() の条件を満たすでしょうから、少しは高速化されるのかもしれません。