std::any 的性能开销:基于 libstd++ 源码分析

C++17 中引入了 std::any,可以非常方便地将任意类型的变量放到其中,做到安全的类型擦除。然而万物皆有代价,这种灵活性背后必然伴随着性能取舍。

std::any 的实现本身也并不复杂,本文将基于 libstd++ 标准库源码 深入解析其实现机制与性能开销。

代价

底层存储

std::any 需要解决的核心问题在于:

  1. 异构数据存储:如何统一管理不同尺寸的对象
  2. 类型安全访问:如何在擦除类型信息后仍能提供安全的类型查询。例如可以直接通过 std::any 提供的 type() 函数,直接获取到底层数据的类型信息。

从 libstd++ 源码中提取的关键类结构如下

1
2
3
4
5
class any {
_Storage _M_storage;

void (*_M_manager)(_Op, const any*, _Arg*);
}

可以看到有两个核心变量:

  • _M_storage:负责存储数据值本身或者指针。
  • _M_manager :函数指针,负责指向具体类型 template class 的实现,其中包含了类型信息。

我们先看 _M_storage 的实现:

1
2
3
4
5
union _Storage
{
void* _M_ptr;
unsigned char _M_buffer[sizeof(_M_ptr)];
};

_Storage 类是一个 union 实现。里面包含两个属性:_M_ptr 和长度为 sizeof(_M_ptr) 的 char 数组 _M_buffer。即长度为指针大小,在 64 位机器下,_M_buffer 的长度是 8。

那么,在什么情况下分别使用 _M_ptr_M_buffer 呢?主要通过以下模板变量进行编译期决策。

1
2
template<typename _Tp, typename _Safe = is_nothrow_move_constructible<_Tp>, bool _Fits = (sizeof(_Tp) <= sizeof(_Storage)) && (alignof(_Tp) <= alignof(_Storage))>
using _Internal = std::integral_constant<bool, _Safe::value && _Fits>;

简单来说:_Tp 可以无异常移动构造 && _Tp 能完全放入 _Storage 中

这是一个非常典型的 SOO(Small Object Optimization 小对象优化)。即:对于小尺寸对象,直接在容器自身的连续内存中 (通常为栈内存) 完成存储,这样可以避免在堆上开辟新的内存。

因此:

  • 对于小尺寸对象(≤指针大小),直接在 _M_buffer 中通过 placement new 创建对象。避免堆内存分配带来的性能开销,提升 CPU 缓存局部性(对高频访问的场景尤为重要)。
  • 对于大尺寸对象,直接在堆上通过 new 申请内存,_M_storage 存储对应的指针。

但这个内存结构的设计,也存在着潜在的内存浪费:union 的内存等于最大字段的内存,因此即使在 std::any 中存储 1 字节的 char 类型变量,_M_storage 也需要 8 字节。

另外,我们发现在 _Storage 并未存储任何类型信息。但我们可以通过 std::any 的 type() 函数获取到对应的类型信息。这是如何做到呢?

接下来,我们看 _M_manager 的实现:
std::any 的做法非常巧妙,将所有需要类型信息的操作,都通过一个 template class 的 static 函数来实现。std::any 对象中只存储这个函数的指针,即 void (*_M_manager)(_Op, const any*, _Arg*)

1
2
3
4
5
6
template<typename _Tp>
struct _Manager_internal
{
static void
_S_manage(_Op __which, const any* __anyp, _Arg* __arg);
};

以 std::any 的 type() 函数实现为例, 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const type_info& type() const noexcept
{
_Arg __arg;
_M_manager(_Op_get_type_info, this, &__arg);
return *__arg._M_typeinfo;
}

template<typename _Tp>
void
any::_Manager_internal<_Tp>::
_S_manage(_Op __which, const any* __any, _Arg* __arg)
{
switch (__which)
{
case _Op_get_type_info:
__arg->_M_typeinfo = &typeid(_Tp);
break;
}
}

我们可以看到,通过_M_manager找到对应template class的具体实现,直接调用typeid(_Tp)就可以获取到对应的 typeinfo 信息了。

但值得注意的是,在调用 _M_manager 函数的时候,额外传递了一个 enum 值 _Op_get_type_info

这是 std::any 的特殊设计,通过枚举值区分不同的逻辑,将所有需要类型信息的操作都整合到一个函数入口。这样做仅用一个函数指针即可,可以节省内存开销。

总结

虽然 std::any 提供了极大的灵活性,且绝大部分场景下性能也够用。但根据我们对源码的深入分析,发现 std::any 的设计特点必然会带来一些额外的开销:

  1. 内存开销:在 64 位机器下固定占用 16 byte 空间(8 字节的_M_storage 和 8 字节的_M_manager 函数指针)。存储 1 字节数据时空间利用率仅 6.25%;
  2. 性能开销:小对象直接栈存储,对于大对象会触发堆分配。