容器模板参数性能对比:多类型元素存储的vector迭代性能分析及替代方案评估
针对你提出的几种存储Symbol派生类的vector方案,我来逐个分析它们的迭代性能,以及你同事的替代方案的优劣:
各个方案的迭代性能分析
1. 先排除不可行的方案:std::vector<Symbol> vec3
这个方案存在对象切片的严重问题:当你把带有Token成员的Terminal存入vector<Symbol>时,只会拷贝基类Symbol的部分,Terminal特有的Token成员会被完全丢弃——这不仅让你的功能逻辑失效,哪怕只看性能,它也因为功能不符合需求而没有讨论价值。
2. 性能最优的候选:你同事的vector<Symbol>合并方案
你的同事把isTerminal标记和Token成员都放进了基类Symbol,这个方案的性能表现是所有选项里最好的,原因如下:
- 内存连续性拉满:vector里的每个
Symbol对象都是连续存储的,迭代时CPU缓存命中率极高,没有任何间接内存访问的开销。 - 类型判断开销可以忽略:判断是
Terminal还是NonTerminal只需要读取一个bool值,这是单周期的内存读取操作,几乎没有额外开销。 - 无类型擦除 overhead:不需要
any或variant的类型信息存储、运行时检查,访问Token成员也是直接的内存访问。
当然这个方案也有缺点:
- 内存浪费:
NonTerminal不需要Token成员,但每个Symbol对象都要为它预留空间,如果你的场景里NonTerminal数量很多,会有额外的内存占用。 - 扩展性差:如果以后需要新增第三种类型的符号,你不得不修改基类
Symbol,违反了开闭原则。
但纯从迭代性能的角度,它是当之无愧的第一。
3. 平衡性能与扩展性:std::vector<std::variant<Terminal, NonTerminal>> vec2
std::variant是C++17引入的类型安全的联合体,它的迭代性能仅次于你同事的方案:
- 内存连续:vector里的每个
variant元素都是连续存储的,大小等于Terminal和NonTerminal中尺寸较大的那个(这里是Terminal,因为带Token),迭代时缓存友好。 - 类型判断开销极低:用
std::holds_alternative或std::visit做类型判断时,编译器会做大量优化,很多情况下可以把类型判断转换成简单的整数比较,运行时开销几乎可以忽略。 - 无堆分配:
variant是值类型,所有数据都存在vector的连续内存里,没有any可能带来的堆内存间接访问。
这个方案的优势是类型安全,扩展性比同事的方案好(新增类型只需要修改variant的模板参数),同时性能非常接近最优解。
4. 性能最差的方案:std::vector<std::any> vec1
std::any的迭代性能是这几个里最差的,原因如下:
- 可能的堆分配:如果
Terminal的大小超过了std::any的小对象优化阈值,它会在堆上分配内存存储Terminal对象,迭代访问时需要通过指针间接访问堆内存,缓存命中率大幅下降。 - 类型转换开销大:用
std::any_cast获取实际类型时,需要运行时检查类型信息(比如存储的type_index),这个开销比variant的类型判断高很多。 - 内存布局的额外开销:
std::any本身需要存储类型信息和对象数据,哪怕是小对象优化,也会比variant多一些内存 overhead。
总结性能排序
从迭代性能由高到低排列:
- 同事的合并
Symbol方案 >std::variant方案 >std::any方案 - 注意:
vector<Symbol>(vec3)因对象切片功能失效,直接排除。
内容的提问来源于stack exchange,提问作者LI.LE
相关产品推荐
相关产品推荐

