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

开门见山好了,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 <ctime>
#include <iostream>
#include <boost/shared_ptr.hpp>
#include <boost/weak_ptr.hpp>

using namespace boost;

int main()
{
clock_t clk_s, clk_e;

shared_ptr<float> 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<float> 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<float> 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 60220 80 90 80 80 90 81 80 90 90
weak_ptr (10次,以空格分开)
51 50 60 50 60 50 50 60 50 50100 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 191230 90 80 90 80 90 81 100 80 90
weak_ptr (10次,以空格分开)
190 190 190 191 180 190 191 190 190 20080 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
}

就可以保证在两种情况下的最优性能。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值