picACG本地缓存目录_Hands-On Design Patterns With C++(十)本地缓存优化(上)

921d50373c73ae91b13eda6a17128993.png

目录:

trick:Hands-On Design Patterns With C++(零)前言​zhuanlan.zhihu.com
0fec78d9ea0b678312fa4a0797059eb8.png

本地缓存优化(上)(Local Buffer Optimization)

并非所有的设计模式都与类继承有关,软件设计模式讨论的通用性与可复用的解决方案,对于C++来说,最常见的一个问题是性能不足。性能不佳的最常见因素是低效的内存管理。本章我们讨论一种针对频繁分配小块内存的优化模型。

本章将会探讨如下几个问题:

  1. 小块的内存分配的开销如何?如何度量开销?
  2. 本地缓存优化是什么?它是如何做到提高性能的?如何衡量优化?
  3. 如何高效地使用本地缓存优化?
  4. 本地缓存优化有什么缺点和限制?

小块儿内存分配的开销

本地缓存优化就只是一种优化,对于优化我们要对于一个规则牢记于心:永远不要猜测或臆想性能有多少提升,必须通过某种手段去衡量出性能优化效果!

内存分配的开销

我们探讨的主题是内存分配的开销以及减少内存分配次数的方法。我们要解决的第一个问题是内存分配的开销有多大!我们仍然使用google的benchmark去测量内存开销:

#include 

最后运行结果如下(测量单入参64):

fd6df2d53045adaa43dea92d07f4666a.png

上述结果的含义是申请并释放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}}); // 注释掉

最终结果如下:

93b19f328476adf3ff3334eaaee2f4a7.png

从结果可以看出,并非我们所想的开销很小,当我们从一个线程增加到两个线程时,内存分配的成本增加了十几倍!这就引出了我们的问题,局部内存不断申请释放其实对性能会产生很大的影响,我们将通过本地缓存优化来解决此问题。

本地缓存优化介绍

程序完成某项任务的最小开销其实就是不做任何操作(都不做了,开销就没有了)

05336cf259bb56a2f9385801c7ed901a.png

本地缓存优化的思想就是减少分配的工作次数,不要随时在malloc,从已有的空余内存中取出一块来使用。

核心思想

本地缓存优化的要点是:内存分配不会孤立发生。通常,如果需要少量内存,则从特定数据结构中分配出一部分小内存,我们从一个小例子入手:

一个字符串类,一般写法如下:

class 

simple_string通过strdup去申请内存,strdup是string.h中的函数,内部通过malloc申请内存。所以每次在赋值时都会malloc出一块新内存。换言之,每次构造字符串时,都会额外分配一次内存,所有的内存都是临时malloc出来的。

我们本章主角,本地缓存优化,我们为什么不从开始就构造一个比较大的对象呢?可能我们不知道我们需要多大的内存,因为用户是可以构造任意长的字符串,这是我们可以使用原来的方式再进行内存重新分配了:

class 

上述代码中,超过15个字符的字串存到s_中,小于的存在b_中,注意重新分配和销毁内存时,要释放原有内存(tag == 1时),根据tag标记使用哪种方式,否则会造成内存泄漏。

本地缓存优化的效果

下面我们继续用benchmark测试,我们分别测试simple_string与small_string,首先是测试构造函数

template

测试结果如下:

721dd69596310655f7c3ae674e6dda01.png

使用多线程测试结果如下:

c9064eb059cd048bd6c33a825c84e844.png

多线程下创建字符串的速度很慢,然而small_string却没有这种代价。

下面测试拷贝和赋值字符串的场景:

template

结果如下:

94ecb19081027a4246fd5898ba34853c.png

对于比较操作,它不涉及内存的申请和释放,简单的字符串对于simple、small两者看不出什么区别。为了看到差异,我们创建多个字符串对象进行比较:

template

结果如下:

08345c8b7210c1f35dd67f09e4bd9e6e.png

对于较小的N值(少量的字符串总数),优化不会带来任何明显的好处。 但是,当我们处理大量字符串时,将字符串与小字符串优化进行比较大约可以快一倍!

为什么会这样?compare内部并没有分配内存。此实验显示了本地缓存优化的第二个非常重要的好处-优化内存位置。 在读取字符串数据之前,必须先访问字符串对象本身,它包含指向数据的指针。对于常规字符串,比较操作需要访问不同且不相关的两个内存地址。如果数据量很大,对字符串数据的第二次访问可能会丢失高速缓存的优势,并等待将数据从主存储器中取出。

而优化的字符串,数据保存在同一块已经预先分配好的位置,所以数据往往保持在高速缓存中。

我们需要大量的字符串才能发现这种优势,原因是少量数据会永远驻留在内存中(释放之前),只有字符串总大小超过缓存大小时,性能优势才能体现出来。

其它细节优化

注意我们的small_string中并没有只保留buf本地缓存区,也保留了原始的char*来保存超过buf范围的字符串,使用tag去表明哪种方式去保存字符串。这样的设计相比与只保留buf的方式通用性更强。

下篇文章,我们将本地缓存优化进行扩展,将至扩展到其他应用场景,比如vector这样的容器、类型擦除及回调对象等,并说明本地缓存优化的缺点。我们下篇再见!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值