目录:
trick:Hands-On Design Patterns With C++(零)前言zhuanlan.zhihu.com本地缓存优化(上)(Local Buffer Optimization)
并非所有的设计模式都与类继承有关,软件设计模式讨论的通用性与可复用的解决方案,对于C++来说,最常见的一个问题是性能不足。性能不佳的最常见因素是低效的内存管理。本章我们讨论一种针对频繁分配小块内存的优化模型。
本章将会探讨如下几个问题:
- 小块的内存分配的开销如何?如何度量开销?
- 本地缓存优化是什么?它是如何做到提高性能的?如何衡量优化?
- 如何高效地使用本地缓存优化?
- 本地缓存优化有什么缺点和限制?
小块儿内存分配的开销
本地缓存优化就只是一种优化,对于优化我们要对于一个规则牢记于心:永远不要猜测或臆想性能有多少提升,必须通过某种手段去衡量出性能优化效果!
内存分配的开销
我们探讨的主题是内存分配的开销以及减少内存分配次数的方法。我们要解决的第一个问题是内存分配的开销有多大!我们仍然使用google的benchmark去测量内存开销:
#include
最后运行结果如下(测量单入参64):
上述结果的含义是申请并释放64字节大概需要18ns。
我们还可以通过benchmark测量一个取值范围,将上述代码Arg(64)改为如下:
// BENCHMARK(BM_malloc_free)->Arg(64); 此行注释,设置下一行
benchmark小贴士:
RangeMultiplier是指基底,上面代码表示取值在32-256的闭区间中,基底为2,即取值为{32, 64, 128, 256},如果没有设置RangeMultiplier,则默认基底为8。取值包含Range左右两侧的值!比如:
BENCHMARK(BM_malloc_free)->Range(10, 1000),默认基底为8,取值带Range左右值,所以取值为{10, 64, 512, 1000}。
下面,我们将测试容器分配和释放需要的时间,并增加一个方法中内存申请释放的次数(通过REPEAT宏定义):
#include
上述代码测试结果与开始的benchmark结果相似,也是差不多54M items/s,看上去速度仍然很快(18ns)。现代CPU往往是多线程的,在一般的印象中,我们认为做的是局部小范围分配内存,每个分配内存的操作很小,所以分配的开销微不足道。然而,我们一定不能猜测推断,实际运行看看结果怎么样呢。
我们如果2个线程并行运行会怎么样?更改如下代码:
// BENCHMARK(BM_malloc_free)->RangeMultiplier(2)->Ranges({{32, 256}, {1<<15, 1<<15}}); // 注释掉
最终结果如下:
从结果可以看出,并非我们所想的开销很小,当我们从一个线程增加到两个线程时,内存分配的成本增加了十几倍!这就引出了我们的问题,局部内存不断申请释放其实对性能会产生很大的影响,我们将通过本地缓存优化来解决此问题。
本地缓存优化介绍
程序完成某项任务的最小开销其实就是不做任何操作(都不做了,开销就没有了)
本地缓存优化的思想就是减少分配的工作次数,不要随时在malloc,从已有的空余内存中取出一块来使用。
核心思想
本地缓存优化的要点是:内存分配不会孤立发生。通常,如果需要少量内存,则从特定数据结构中分配出一部分小内存,我们从一个小例子入手:
一个字符串类,一般写法如下:
class
simple_string通过strdup去申请内存,strdup是string.h中的函数,内部通过malloc申请内存。所以每次在赋值时都会malloc出一块新内存。换言之,每次构造字符串时,都会额外分配一次内存,所有的内存都是临时malloc出来的。
我们本章主角,本地缓存优化,我们为什么不从开始就构造一个比较大的对象呢?可能我们不知道我们需要多大的内存,因为用户是可以构造任意长的字符串,这是我们可以使用原来的方式再进行内存重新分配了:
class
上述代码中,超过15个字符的字串存到s_中,小于的存在b_中,注意重新分配和销毁内存时,要释放原有内存(tag == 1时),根据tag标记使用哪种方式,否则会造成内存泄漏。
本地缓存优化的效果
下面我们继续用benchmark测试,我们分别测试simple_string与small_string,首先是测试构造函数:
template
测试结果如下:
使用多线程测试结果如下:
多线程下创建字符串的速度很慢,然而small_string却没有这种代价。
下面测试拷贝和赋值字符串的场景:
template
结果如下:
对于比较操作,它不涉及内存的申请和释放,简单的字符串对于simple、small两者看不出什么区别。为了看到差异,我们创建多个字符串对象进行比较:
template
结果如下:
对于较小的N值(少量的字符串总数),优化不会带来任何明显的好处。 但是,当我们处理大量字符串时,将字符串与小字符串优化进行比较大约可以快一倍!
为什么会这样?compare内部并没有分配内存。此实验显示了本地缓存优化的第二个非常重要的好处-优化内存位置。 在读取字符串数据之前,必须先访问字符串对象本身,它包含指向数据的指针。对于常规字符串,比较操作需要访问不同且不相关的两个内存地址。如果数据量很大,对字符串数据的第二次访问可能会丢失高速缓存的优势,并等待将数据从主存储器中取出。
而优化的字符串,数据保存在同一块已经预先分配好的位置,所以数据往往保持在高速缓存中。
我们需要大量的字符串才能发现这种优势,原因是少量数据会永远驻留在内存中(释放之前),只有字符串总大小超过缓存大小时,性能优势才能体现出来。
其它细节优化
注意我们的small_string中并没有只保留buf本地缓存区,也保留了原始的char*来保存超过buf范围的字符串,使用tag去表明哪种方式去保存字符串。这样的设计相比与只保留buf的方式通用性更强。
下篇文章,我们将本地缓存优化进行扩展,将至扩展到其他应用场景,比如vector这样的容器、类型擦除及回调对象等,并说明本地缓存优化的缺点。我们下篇再见!