TL;DR
https://github.com/noodle1983/llvm-ADT
1. 简要模型
栈上容器,其实也不太对,他其实是指预分配内存的一类容器,只是他通常作为临时变量在函数内使用,编译期就确定了大小,构造时不使用堆内存(new/malloc),直接在栈上构建,随函数退出析构。大概的实现:
1 | class XXX{ |
这样就比较好理解下面这句的内存分配了。
1 | void hello_world(){ |
这种情况也比较好理解:作为一个字段放在另外一个对象里去new一个对象,这段内存就不在栈上了。
1 | class ABC{ |
如果大小超过了预分配内存,会如何?如果超过了,会退化成std相应容器。
2. 使用场景
- 临时对象,用完就丢的场合,没必要做内存泄漏方面的校验和检查,减少内存分配器压力,对用着内存校验的堆分配器会有明显的改进。
std平替容器
std | 栈容器 | 补充说明 |
---|---|---|
std::vector | llvm::SmallVector | |
std::string | llvm::SmallString | |
std::set | llvm::SmallDenseSet | 对象个数限制2的n次幂 |
std::map | llvm::SmallDenseMap | 对象个数限制2的n次幂 |
2的n次幂的限制源于key的hash取Bucket index的Mask,改成mod应该能把他去掉,因为如果元素hash mask后是散列的,那么取余也是散列的。改了之后理论上会慢一点(但大多数时候不需要计较的那种)。我用的地方只有几处,没改。
3. 源码
源码在llvm的ADT目录下, 和llvm其他代码有耦合。
我把他做了剥离,参见https://github.com/noodle1983/llvm-ADT,Linux/Windows测试和使用没什么问题。
除了栈上容器,还有其他的一些bit容器,稀疏容器等好东西,我没用的上。
用法参考源码里面的unittest,可以打开VS工程下个断点看看,Linux我直接集成进游戏工程了,没去折腾unittest。
4. 背后的故事
吹牛B时间。
我对它的认识还是刚毕业在爱立信的时候,做的第一个项目是基于Sun Solaris的,这个操作系统各方面的文档超全。在工作之余,一边读文档,一边拿项目做实验。印象很深的一个,是统计各个函数的总耗时,看看哪个函数比较耗,看看有什么改进的。结果出乎我的意料,最耗的函数,竟是std::string构造函数的内存分配,而且和第二名是数量级的差别。具体数据因历史久远就不可考了(当时的内存分配器比较弱,线程级的分配器因为有bug没开,估计加剧了这种差异),只是这个项目是一个电信的计费网关,没有密集的字符串处理,最多就是写日志和话单,所以当时印象深刻,却又束手无策,就惦记上了。
在爱立信的第二个项目,一个http网关,我神奇地发现,项目自己实现了一个MyString对象,预分配的64字节的空间,超出后退化为std::string。使用过程中发现,栈上这么一个MyString对象,64字节就在栈上,大部分时候,字符串都很短小,因此不会像堆申请任何内存,随着函数的退出,栈的弹出,自然就析构释放了,十分的精巧。
之后几年线程级内存分配器很快成熟,这块的消耗好像也不算什么了。仅在广州西山居的服务器后台见到,有种似曾相识的感觉。
辗转到游雁游戏,后端用着一个纯c++的游戏服务器,没带任何的内存检查设施,遂开发环境挂上Address Sanitizer。不出意料,卡。100毫秒的掉帧监控发现内存分配和释放都卡,尤其是释放有一个校验内存合法性的循环十分卡。本着有困难要上,没有困难创造困难也要上的精神,想先把大量的临时字符串内存用预分配的方法去掉。再本着别人有,就不用自己做的精神在Google/Github上搜。确实有一个SmallString,再朔源,发现是llvm的ADT(advanced data type)的单个文件剥离版。那段时间刚好在centos6上编译了最新的llvm,一看源码,我就乐了,不单止string有SmallString,vector/set/map都有相应的实现,和之前在爱立信看到的思路一样,超了就退化到std库,比较灵活的一点是预分配元素的个数可以通过模板参数提供。当然,还有其他的侵入式容器,bit相关容器等。
好东西,只是这个库依赖于llvm的其他组件,遂做移植,裁剪,linux/windows下可用。