[译]内存分配的隐藏成本

Posted by Deadline on February 12, 2015

理解内存分配的成本很重要,但测量这一成本又需要很多技巧。通过定时调用new[]和delete[]来测量似乎是个不错的方法。然而对于大型缓冲区,定时器可能会遗漏99%以上的真实成本,这些隐性成本可能比我预期的更大。

我们来看看更复杂的测量,事实证明,有些成本可能转移到另外一个进程,尽管测量似乎很合理,但看不到任何结果。本文(特指在Windows环境)我将解释这些隐性成本是什么,他们如何隐藏,如何衡量它们,应该怎么做。

快速测试

下面这些释放和重新分配内存的成本,哪些需要你完整的记录,而不是反复使用它们?

重新分配和释放内存的时间。 使用内存的时间。 系统进程消耗的CPU时间。 以上全部。 剧透警告,答案是#4。对于足够大的内存块来说,重新分配和释放的时间和分配处理相关操作的CPU开销来说,只是一部分。

我承认我喜欢大内存

在这篇文章中我只关注大内存的分配—大到可以分配给操作系统。为了专心的关注操作系统分配大内存的问题,我们就忽略那些堆碎片等细小的问题了。这些问题是真实存在的,我遇到过好几次了,修复这些问题很容易就能提升性能,但是我不想在这篇文章中把所有的内存分配问题都列举出来。

测量分配和释放成本的方法

测量分配和释放的成本似乎很简单。在一个循环中,测量不同大小的内存块new[]/delete[]操作需要多长时间。在我的测试中,内存大小从8MB至32MB,new[]/delete[]操作的成本平均大概7.5μs(微秒),其中约5μs花在了分配,约2.5μs花在了释放。在测试中发现分配的大小似乎并没有明显的影响结果。

释放将变得缓慢

分配了内存却不使用它有点太假了,所以接下来在释放内存之前,我们要将它写进整个内存块中。停下来想想,这会对分配和释放内存的成本产生什么影响呢。我们拭目以待。

这种情况下分配内存的成本略有上升,大概是因为写内存将会从CPU缓存中刷新各种有用的数据,使得随后的分配稍微慢了些,这增加8μs的分配成本。

释放内存的成本就变的更贵了。写内存之前释放成本大概是每MB75μs,相比之前释放未使用的内存成本约2.5μs,是一个巨大的提升。这意味着释放32MB使用过的内存要用2400μs(2.4ms)!

内存,按需分配

这看似奇怪的行为背后,是因为Windows操作系统的懒惰。对于大内存的分配(1MB或以上)Windows内存堆(new/new[], malloc/HeapAlloc)会使用系统的虚拟分配(VirtualAlloc)功能(with MEM_COMMIT MEM_RESERVE)来请求内存。这将保留地址空间和分配提交指令,但是实际上并不提交任何page。由虚拟分配(VirtualAlloc)返回的指针基本上就是个承诺—承诺当page被访问时,一定会有内存给你。就像文档中说的“实际上物理内存页是没有分配的,除非虚拟地址被实际访问了”这是一件好事,因为应用程序申请的内存可能比实际需要的内存多,这样能减少浪费,Linux也有类似的机制。

所以,分配释放那些没有被操作命中的内存成本是非常小的。但是如果内存被命中了内存页也就不连续了,释放这些内存实际上要删除它们。从进程地址空间中删除这些内存页需要每MB75μs。

Faulty towers

如果从进程地址空间删除这些内存页需要75μs每MB,那将内存页放进地址空间也需要一定的时间。这种成本很难去测量,因为只有在第一次访问内存页真正请求内存的时候才会发生。最简单的测量方法就是测量下写入新分配的内存块需要多长时间,然后和写入之前分配的内存块时间相比较。

这种方法比较冒险。测量完可能得到不同的结果。如果内存块太小,那测量的可能是缓存的效果。除此之外,还可能受到TLB( Translation Lookaside Buffer)的影响。如果没有足够的内存那么内存页会在工作区中被删除,然后在需要的时候被替换回来。还有其他的一些进程也很容易影响到结果。像CPU运行时的频率变化就会干涉到结果。

但是,我尽了我最大的努力来避免各种干扰。我的CPU三级缓存(L3)是6MB的,所以我测试从8MB至32MB,尽量避免缓存的影响。我的内存很大,也关闭了很多无关的进程来避免影响到结果。我测试了很多次。我用了高性能电源模式,还在后台跑了一个线程来保证CPU频率一直保持在一个较高的水平。我还将ETW (xperf) 配置记录下来了,检查它们来找出错误源。

结果很清楚。不连续的内存页第一次使用的成本最小大概175μs每MB。某些情况下,成本会变得很高,至于原因还不太清楚,但是对于分配8MB以上的内存,第一次使用的时候你可以假设最小的成本就是约175μs每MB。

软页错误,硬页错误

软页错误和硬页错误之间是有区别的。软页错误是内存缺页造成的。新分配的内存页第一次被引用,或在备用列表中被发现有缺陷—可能从工作集中被修整过,或者映射文件在磁盘缓存上。软页错误成本虽然昂贵,但是在这种情况下,还是算相当便宜的。

硬页错误的发生跟磁盘有关,也许内存页在内存映射文件中或者从交换文件中恢复数据。这些成本是非常昂贵的,大家都知道磁盘读写速度是非常慢的。硬页错误很容易就导致成本超过1s每MB,但是那些是硬错误,本文主要关注那些很便宜的软页错误。

(想了解更多 http://en.wikipedia.org/wiki/Page_fault)

归零

但是,等等,还有一个我们至今未提的内存分配成本。

出于安全考虑,新映射的内存页都将归零,否则会在进程之间泄露信息。所有的现代操作系统都会有这个操作。内存页归零操作的成本不是很贵,但也不便宜。

Windows操作系统试图保证一个可用的归零内存页池,可以快速分配给那些发生内存故障的进程。但是这个内存页池需要被释放的内存页和归零内存页来补充。这意味着系统进程有一个低优先级的线程来将那些释放的内存也归零。如果它是一直持续运行的,那么意味着所有被释放的内存归零操作都是在你的程序进程之外完成的,这部分成本被隐藏了,你无法测量到。

想要了解更多关于内存页生命周期的信息可以看看这个网页。

但是没有什么可以逃过ETW (xperf)的“火眼金睛”。CPU使用率被曲线图精确的展示出来了,红色的是测试进程,绿色的是系统进程。每个测试循环结束时,测试进程释放了内存,你可以看到系统进程的“生命轨迹”。

enter image description here

这意味着只有一种方法来测量那些微小的内存分配成本,用WTP同时记录下你的进程和系统进程的“轨迹”。

如果归零线程无法跟上节奏,那内存页归零操作可能在你的进程中进行,不过在KeZeroMemory中可以看得出来,这种情况一般发生在那些

CPU计算能力较弱的机器上。

CPU使用率(采样)数据会告诉你归零线程在干什么。然后你会发现线程8一直都是归零线程,可以用CPU使用率(精确)数据来测量它每次唤醒工作的时长。被命中然后释放掉时长大概是150μs每MB。

总成本

总之,简单的测量分配释放大内存块的成本可能会得到这样一个结果:分配/释放这样一组操作只需要约7.5μs。事实上对于较大内存的分配却有三个部分的成本。就像表格所展示的,这些加起来大约400μs每MB。每帧分配8MB缓冲区(可能用来存放1080p RGBA 图像)很容易就消耗掉每帧CPU时间3.2ms(μs)。像这样一些成本隐藏在系统进程中,它们可能对那些多核计算机性能没有影响,但是对于双核的机器还是有一定影响的。

值得一提的是,这些数字大多都是取的最低值。尤其是在内存页故障的时候,有时候成本会高的多,至于原因还不是很清楚。

enter image description here

实时监控归零内存页

想要准确可靠的知道你的进程在干什么,性能如何,使用ETW跟踪记录是个不错的办法。我记录了很多我也学到了很多。最近我养成了一个习惯,就是在我的进程中找KiPageFault ,观察归零线程是否有大量活动。

但是这比较枯燥而且耗费时间。实时监控归零线程的活动非常有帮助。这个线程有活动说明有大量新的内存被分配,我们很容易就能联想到。

事实证明,这种监控是很容易。

我提供的信息使用到了一些未证实的Windows操作系统的细节,不同版本可能会有所不同。这些信息仅用于诊断目的。现在,这些所有权归我,如果你发布的产品中使用了这些信息,我会叫上Raymond Chen 组成一个团队来维权,并且会在你的编译器里打乱这些字节码。

我观察到(见上文免责声明)在我的测试中,归零线程的ID总是8。因此我们可以很容易的监视归零线程的活动水平:

用OpenThread 来处理8号线程 用QueryThreadCycleTime 来获取该线程的生命周期 在循环中延迟1000ms并在最后一秒中打印出归零生命周期的消耗 就这么简单。程序要以管理员权限运行,这明显像是一个危险的攻击程序,但是它能帮我们识别出那些让系统频繁重新分配内存的代码。如果一运行你的代码,归零线程就变的很忙碌。那么你的程序可能需要跟踪一下,看看有没有KiPageFault。

测试步骤

所有的测试都是在我的电脑上完成的,电脑配置为四核八线程Core(TM) i7-2720QM CPU@ 2.20 GHz (可睿频至 3.3 GHz) 8G内存,操作系统为Windows 7 SP1。对比测试在Windows 8.1 上完成的,结果是相似的。

enter image description here

粗糙的测试代码

Linux

我在Linux中也做了些对比测试,但是没法精确的测量到进出内存映射页的成本。我看到内核归零内存并在第一次写后映射——运行 perf top 命令,可以看到clear_page_c_e 或者类似的方法。搜索了一下发现之前开发人员已经注意到clear_page_c_e的成本了。Linux似乎是按需清理内存页,而不是有一个专门的线程, 这两种方式各有优缺点。

相关文章

Making VirtualAlloc Arbitrarily Slower

64-Bit Made Easy

如果你想了解更多关于怎么使用ETW来深入的研究Windows性能这类信息,那么我推荐xperf系列文章,其中包括各种教程和文档。尤其是Wintellect Now训练视频,可能这是我创建的最好的资源了,可以通过这免费的观看。

转载自伯乐在线


There are no comments on this post.