您的位置:首页 > 其它

boost 智能指针 --- 关于性能的少数派报告

2008-04-08 23:17 357 查看
开门见山好了,boost 1.33 对于 boost 1.32 的 shared_ptr 和 weak_ptr 有一个不小的改变,然而这个改变如此透明,以至于它甚至于没有出现在 boost  1.33 的 release notes 中。

在 1.32 中,shared_ptr 和 weak_ptr 的引用计数使用锁来保证线程安全,下面一段摘自 boost 1.32 的 sp_counted_base 实现,大家可以在 boost 的 detail/shared_count.hpp 中找到它:

    void add_ref_copy()
    {
#if defined(BOOST_HAS_THREADS)
        mutex_type::scoped_lock lock(mtx_);
#endif
        ++use_count_;
    }

    void add_ref_lock()
    {
#if defined(BOOST_HAS_THREADS)
        mutex_type::scoped_lock lock(mtx_);
#endif
        if(use_count_ == 0) boost::throw_exception(boost::bad_weak_ptr());
        ++use_count_;
    }

其中 add_ref_copy 是用于shared_count ,后者是用于 shared_ptr 的引用计数器;add_ref_lock 用于 weak_count ,后者是用于 weak_ptr 的引用计数器。

可以看到,在多线程环境中,boost 1.32 用 scoped_ptr 来保证引用计数器访问的串行性。但是在 boost 1.33 中,情况却大大不同了,它使用的是 lock free 的算法,在 Win32 平台下面就是通过操作系统的 InterlockedIncrement / InterlockedDecrement 以及 InterlockedCompareExchange 来完成。下面的代码来自 boost 1.33 的 sp_counted_base 实现,位于 boost 的 detail/sp_counted_base_w32.hpp 文件中:

    void add_ref_copy()
    {
        BOOST_INTERLOCKED_INCREMENT( &use_count_ );
    }

    bool add_ref_lock() // true on success
    {
        for( ;; )
        {
            long tmp = static_cast< long const volatile& >( use_count_ );
            if( tmp == 0 ) return false;
            if( BOOST_INTERLOCKED_COMPARE_EXCHANGE( &use_count_, tmp + 1, tmp ) == tmp ) return true;
        }
    }

上面的 BOOST_INTERLOCKED_INCREMENT 和 BOOST_INTERLOCKED_COMPARE_EXCHANGE 都位于 boost 的 detail/interlocked.hpp 文件中,如大家所料,非常简单:

# define BOOST_INTERLOCKED_INCREMENT InterlockedIncrement
# define BOOST_INTERLOCKED_DECREMENT InterlockedDecrement
# define BOOST_INTERLOCKED_COMPARE_EXCHANGE InterlockedCompareExchange

这种变化意味着什么?最起码,lock free 算法为 boost 智能指针带来了非锁定语义。同时,由于 InterlockedCompareExchange 是一个 CAS (Compare And Swap) 操作,它可能由于读 - 写操作之间有其它线程介入而导致失败,为了能从这种失败中恢复过来, boost 1.33 代码中使用了那个 for(;;) 循环,也就是无限的重试以保证引用计数操作成功。在性能方面,由于 Interlocked 系列的系统调用都利用了硬件的原子操作,在性能上应该会比 scoped_lock 有一定的提高,但是循环所引入的额外操作又对性能有不良影响,究竟孰轻孰重,我们下面就在测试中检验。

测试程序非常简单,我尽量不引入无关的操作以免影响结果:
(代码文件:sp.cpp)

#include
#include
#include
#include

using namespace boost;

int main()
{
  clock_t clk_s, clk_e; 
 
  shared_ptr fp(new float(3.14f));
  for(int i = 0; i < 10; ++i)
  {
    clk_s = std::clock();
    for(int j = 0; j < 1000000; ++j)
    {
      shared_ptr fp1 = fp;
    }
    clk_e = std::clock();
   
    std::cout << clk_e - clk_s << std::endl;
  }
 
  std::cout << std::endl;
 
  for(int i = 0; i < 10; ++i)
  {
    clk_s = std::clock();
    for(int j = 0; j < 1000000; ++j)
    {
      weak_ptr wp1 = fp;
    }
    clk_e = std::clock();
   
    std::cout << clk_e - clk_s << std::endl;
  } 
}

我在同一台机器的两套环境下编译这个程序,这两套环境唯一的不同只在于一个使用 boost 1.32 ,一个使用 boost 1.33。编译命令是最简单的一条:

cl /EHsc sp.cpp

注意,这里没有用 /MT 开关,意味着我是在单线程状态下编译,后面会用另外的指令编译,比较其结果是饶有趣味的。

  boost 1.32
 boost 1.33
 shared_ptr (10次,以空格分开)
 150 60 50 60 50 60 50 60 70 60 220 80 90 80 80 90 81 80 90 90
 weak_ptr (10次,以空格分开)
 51 50 60 50 60 50 50 60 50 50 100 90 80 80 91 80 80 90 80 90
我知道一幅图抵得上一千个字,好在用 Excel 做个图也很轻易,下面是这些数据的图表显示:





可以看到,由于在单线程环境下,boost 1.32 的 shared_ptr 引用计数器默认操作实际上是

++use_count_;

而 boost 1.33 则实际上是

InterlockedIncrement( &user_count_ );

后者带来了平均 46.4% 的运行时间延长。对于 weak_ptr ,boost 1.32 中的操作是

if(use_count_ == 0) boost::throw_exception(boost::bad_weak_ptr());
++use_count_;

而 boost 1.33 的操作是

for( ;; )
{
  long tmp = static_cast< long const volatile& >( use_count_ );
  if( tmp == 0 ) return false;
  if( InterlockedCompareExchange( &use_count_, tmp + 1, tmp ) == tmp ) return true;
}

后者带来的运行时间的增加平均达到 62.1% 。这的确是一个值得注意的问题。

接下来我换用另外的编译命令:

cl /EHsc /D"BOOST_HAS_THREADS" sp.cpp

这仍然是单线程,但是我通过定义 BOOST_HAS_TREADS 宏强制 boost 1.32 使用 scope_lock ,以模拟其在多线程下的表现,同时排除其他因素的干扰。

  boost 1.32
 boost 1.33
 shared_ptr (10次,以空格分开)
 380 190 211 190 200 190 201 190 200 191 230 90 80 90 80 90 81 100 80 90
 weak_ptr (10次,以空格分开)
 190 190 190 191 180 190 191 190 190 200 80 90 80 80 91 80 90 80 90 80
当然,还有图:





我们看到,定义 BOOST_HAS_TREADS 宏对于 boost 1.32 的智能指针性能影响是巨大的,而对于 boost 1.33 ,考虑到测量误差,可以说是没有影响。因为在这种情况下,boost 1.33 的引用计数操作没有变化,而 boost 1.32 的 shared_ptr 引用计数变成了

mutex_type::scoped_lock lock(mtx_);
++use_count_;

而 weak_ptr 的引用计数变成了

mutex_type::scoped_lock lock(mtx_);
if(use_count_ == 0) boost::throw_exception(boost::bad_weak_ptr());
++use_count_;

也就是至少多出了一个 scoped_lock 的构造和析构成本,比起 boost 1.33 ,这个时候的 boost 1.32 shared_ptr 平均运行时间多出了约 112% ,weak_ptr 的平均运行时间多出了约 126.2% !我们终于看到了新的实现在性能上的好处。

结论:

新的 lock free 算法使得多线程环境下的 boost 智能指针性能大大提高,但是也带来了单线程环境下的一些性能成本,有没有办法两全其美呢?其实很简单,只要沿用过去的做法,用宏来区别即可:

    void add_ref_copy()
    {
#if defined(BOOST_HAS_THREADS)
        BOOST_INTERLOCKED_INCREMENT( &use_count_ );
#else
        ++use_count_;
#endif
  }

以及

    bool add_ref_lock() // true on success
    {
#if defined(BOOST_HAS_THREADS)
        for( ;; )
        {
            long tmp = static_cast< long const volatile& >( use_count_ );
            if( tmp == 0 ) return false;
            if( BOOST_INTERLOCKED_COMPARE_EXCHANGE( &use_count_, tmp + 1, tmp ) == tmp ) return true;
        }
#else
        if(use_count_ == 0) return false;
        ++use_count_;
#endif
  }

就可以保证在两种情况下的最优性能。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: