学习LLVM数据结构:SmallVector
在现代 C++ 程序开发中,选择合适的数据结构,对于程序性能和内存安全都至关重要。LLVM 项目中,有一大块内容便是设计用于内部开发的高效数据结构。
本文将以 cppreference 的格式,介绍这些实用的数据结构。你既可以将其作为学习参考,也可以作为学习 LLVM 的补充材料。
基本介绍
llvm::SmallVector 定义在 llvm/ADT/SmallVector.h 头文件中。
它的声明为:
1 | template<typename T, unsigned N = CalculateSmallVectorDefaultInlinedElements<T>::value> |
llvm::SmallVector 是一个可变长数组,类似于 std::vector,同时它对较小长度的数组做了优化。
它的内存管理方式采用局部缓存的设计思路,在对象内部预留一小块空间,用于存储数据。当数据量超出预留空间的大小时,才会将数据放在堆上。它本身保存一部分元素,这便使得在小数组中,避免进行堆分配的操作,提高了效率。
注意到,它带有一个含默认值的模板参数 N,它用来指定预留空间的大小,默认不指定时,编译器会自动选择一个合理的阈值(通常考虑依据是栈空间的占用情况)。
特点
- 顺序容器:
SmallVector是一个顺序容器,可以在末尾添加和删除元素,可以通过下标访问任意元素。 - 小长度数组优化:它结合了定长数组在栈上快速分配和动态数组在堆上的灵活性,当管理的数据长度小于阈值(默认值或声明时给定值),会将数据保存在栈上,从而避免堆分配和内存管理带来的性能开销。
- 操作一致:它的大多数操作,和 STL vector 保持一致。
与标准库 vector 的对比
- 性能优势:在数据量较小时,由于不会涉及到堆内存分配和管理的开销,性能会优于
std::vector。另外,它可以识别平凡可复制的特性,从而更细粒度地做内存管理。 - 退化时的性能损失:当发生从栈到堆的退化时,
llvm::SmallVector会带来数据拷贝的开销。所以需要仔细考虑阈值的设定。 - 可能浪费空间:
llvm::SmallVector会在定义时预分配设定的N的空间,如果实际数据少于N,那么会存在空间浪费的问题。std::vector也存在类似问题。
继承结构
简单的继承结构如:

1 | template <typename T, unsigned N> |
SmallVector 继承自 SmallVectorImpl 和 SmallVectorStorage,其中 SmallVectorImpl 实现了大多数通用的操作代码和管理长度等属性的数据成员。它继承的 SmallVectorTemplateBase、SmallVectorTemplateCommon 和 SmallVectorBase 都是用来就完成模板实例化时的公共代码,这里先按下不表。SmallVectorStorage 实现了堆内联存储的静态数组的分配和管理,它是一个 POD 类型,只含有数据。这也有利于编译器为特定长度的数组做专门的优化。
通过这种分离,SmallVectorImpl 可以用于不同模板类的复用,而 SmallVectorStorage 又可以方便的做模板特化(不同的 N)。这样,减少了模板实例化时的实例化数量,降低了编译空间占用,提高编译效率,是一种在很多 C++ 工程中普遍用到的实现技巧。
我们再回头看 SmallVectorImpl 的继承结构,它的父类中,SmallVectorBase 作为最底层的类,提供了最基本的功能,包括类型的数据,比如 size 和 capacity,一些辅助函数和虚函数定义。它作为整个继承体系的根,定义了所有 SmallVector 最通用的接口和行为。SmallVectorTemplateCommon<T, void> 抽取了 SmallVectorTemplateBase 的实现,它实现了不依赖于 T 是否是 POD 类型的代码。而与 POD 类型相关的代码实现,放在了 SmallVectorTemplateBase 中。SmallVectorTemplateBase 针对的就是非平凡的类型,根据是否可复制、可移动,又实例化了不同的模板类。它在正确性和性能之间达成平衡,在避免错误的同时,为大多数非平凡类型提供在元素操作上的最优性能。
数据成员
忽略多层级的继承结构,整个 SmallVector 中包含几个主要的成员:
1 | class SmallVectorBase { |
这 3 个成员决定了索引数据和判断优化阈值。
另外,对于小长度数组,还需要分配内置的存储空间:
1 | template <typename T, unsigned N> |
成员方法
这里罗列了部分常用的方法,完整列表和实现细节请参考 llvm/ADT/SmallVector.h 文件代码。
构造
1 | // 创建一个空的容器,inline storage 大小为 4 |
迭代器
和 std::vector 一样,也提供了前向迭代器和反向迭代器,以及对应带 const 的版本。
1 | llvm::SmallVector<int, 4> vec = {1, 2, 3, 4, 5}; |
元素访问
1 | llvm::SmallVector<int, 4> = {1, 2, 3, 4, 5}; |
比较
支持常见的几种比较运算符,其中,大于、小于操作的比较逻辑采用通用的序列比较算法,在序列长度相同时,比较对应元素的大小或字典序。
元素操作
基础元素操作与 std::vector 相同。
1 | llvm::SmallVector<int, 4> vec; |
构建
使用 llvm::SmallVector 容器,需要引入 Support 组件,如:
1 | set(LLVM_LINK_COMPONENTS Support) |
注意事项
不保证异常安全
SmallVector 本身并不是异常安全的。这一点和 std::vector 不同,标准库容器能保证异常安全性。
不合适的预分配大小
如果在定义 SmallVector 时,指定栈上预分配容量 N 非常大,可能会导致栈溢出,需要根据实际情况选择合适的 N ,避免栈溢出。
如果预分配容量 N 不合理,导致使用容器对象时,总是会发生退化到堆的问题,那么重复的数据拷贝,也会带来性能问题。
迭代器失效
和其他顺序容器类型一样,使用迭代器时,插入和删除操作可能会导致迭代器失效。尤其是还需要考虑因为插入元素导致 SmallVector 重新分配内存(栈到堆的退化和堆到堆的重新分配),迭代器都会失效。
resize 操作也会导致迭代器失效。
自定义类型的内存管理
如果 SmallVector 存储的是自定义类型,那么需要确保自定义类型有着正确的构造、移动和拷贝操作,不正确的实现会导致内存泄漏等问题。
如果自定义类型是引用类型,那么使用 SmallVector 管理时,需要确保自定义类型的析构函数中,能够正确释放内存。如果 SmallVector 管理的是指针,还需要自定义析构器来处理指针指向的内存。
使用 SmallVectorImpl 作为参数类型
如果需要传递一个 SmallVector 的容器对象时,形参参数建议使用 SmallVectorImpl,后者没有带有 SmallVectorStorage,所以不会实际分配空间,同时也不需要声明函数时指定 N 值,这无论从效率角度看,还是使用便利性,都非常有价值。比如:
1 | // 这种声明需要指定 N |
性能
不适合用于存储大量元素
虽然 SmallVector 可以在堆上动态分配内存,但它的设计关注点还是在管理小数据量的动态数组。如果已知数量比较大的动态数组,使用 std::vector 即可。
同理,也不适合管理单个元素内存较大的数据类型。
拷贝成本
使用 SmallVector 需要多留意它可能发生从内部存储到堆上退化时的性能问题。尤其是元素类型本身的拷贝开销比较大时,一个合适的 N 就显得尤为重要。
请保证你的设计中,大多数情况下,都不会发生这种拷贝。
添加和删除元素
添加和删除元素会带来性能开销,一方面是为了保证顺序性,操作位置之后的元素需要移动位置;另一方面,如果发生向堆的退化或堆内存的重新分配,也会带来性能问题。
尽量使用 emplace_back 代替 push_back,以避免不必要的拷贝或移动操作,提高性能。
另见
无。
参考资料
- [LLVM Programmers Manual](LLVM Programmer’s Manual — LLVM 21.0.0git documentation)
- [cppreference vector](std::vector - cppreference.com)
- [CHUNer:LLVM 数据结构 - 2.1: llvm::SmallVector 简述 & llvm::SmallVectorBase 类源码详细解析](LLVM 数据结构 - 2.1: llvm::SmallVector 简述 & llvm::SmallVectorBase 类源码详细解析 - 知乎)
- CompilerCoder:编译器(llvm)中的数据结构与设计模式中的数据结构与设计模式 - 知乎](https://zhuanlan.zhihu.com/p/418357950))
- LLVM 16.0 源码
- LLVM 20.0 源码
本文同步发布在知乎账号下:学习LLVM数据结构-SmallVector - 知乎











