为什么进程的内存分配速度慢,速度可能会更快?
我比较熟悉虚拟内存是如何工作的。所有进程内存被分成页面,虚拟内存的每一页都映射到实内存中的页面或交换文件中的页面,或者它可以是新页面,这意味着物理页面仍未分配。 OS根据需要将新页面映射到实际内存,而不是当应用程序要求使用malloc
的内存时,而仅在应用程序实际从分配的内存中访问每个页面时。但我仍然有问题。为什么进程的内存分配速度慢,速度可能会更快?
我注意到了这一点时,正在与我的应用程序与Linux perf
工具分析。
有时间约为20%了核心功能:clear_page_orig
,__do_page_fault
和get_page_from_free_list
。这比我预期的要完成这项任务要多得多,我已经做了一些研究。
让一些小例子入手:
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#define SIZE 1 * 1024 * 1024
int main(int argc, char *argv[]) {
int i;
int sum = 0;
int *p = (int *) malloc(SIZE);
for (i = 0; i < 10000; i ++) {
memset(p, 0, SIZE);
sum += p[512];
}
free(p);
printf("sum %d\n", sum);
return 0;
}
让我们假设memset
只是一些记忆的约束处理。在这种情况下,我们只分配一小块内存并重复使用它。我会这样运行此程序:
$ gcc -O1 ./mem.c && time ./a.out
-O1
必需的,因为clang
与-O2
完全消除了环路和即时计算的值。
结果是:user:0.520s,sys:0.008s。根据perf
,这段时间的99%在memset
从libc
。因此,对于这种情况,写入性能大约为20千兆字节/秒,这比我的内存理论性能高出12.5千兆字节/秒。看起来这是由于L3 CPU缓存。
让变化的测试,并开始分配环路内存(我不会重复的代码相同的零部件)
#define SIZE 1 * 1024 * 1024
for (i = 0; i < 10000; i ++) {
int *p = (int *) malloc(SIZE);
memset(p, 0, SIZE);
free(p);
}
结果是完全一样的。我相信free
实际上并没有为OS释放内存,它只是将它放入进程中的一些空闲列表中。在下一次迭代中,malloc
只是获得完全相同的内存块。这就是为什么没有明显的差异。
让我们从1兆字节开始增加SIZE。执行时间会逐渐增加,并且会在10兆字节附近饱和(对于我来说,在10到20兆字节之间没有区别)。
#define SIZE 10 * 1024 * 1024
for (i = 0; i < 1000; i ++) {
int *p = (int *) malloc(SIZE);
memset(p, 0, SIZE);
free(p);
}
时间显示:用户:1.184s,sys:0.004s。 perf
仍然有99%的时间报告在memset
,但吞吐量约为8.3 Gb/s。那时候,我明白了究竟是怎么回事,或多或少。
如果我们将继续增加内存块大小,在某些时候(对于我在35 Mb上),执行时间将显着增加:用户:0.724s,sys:3.300s。
#define SIZE 40 * 1024 * 1024
for (i = 0; i < 250; i ++) {
int *p = (int *) malloc(SIZE);
memset(p, 0, SIZE);
free(p);
}
根据perf
,memset
将消耗只有18%的时间。
显然,存储器从OS分配和释放每个步骤。正如我之前提到的,OS在使用之前应该清除每个分配的页面。因此clear_page_orig
的27.3%看起来并不特别:对于清晰存储器,它只有4s *0.273≈1.1秒 - 与第三个例子相同。 memset
花了17.9%,这导致约700毫秒,这是正常的,因为在clear_page_orig
(第一个和第二个例子)之后已经存在于L3缓存中的存储器。
我无法理解 - 为什么最后一种情况比内存+ memset
只有memset
慢3倍?我可以用它做点什么吗?
结果在本机Mac操作系统,Vmware下的Ubuntu和Amazon c4.large实例中具有可重现性(差异很小)。
另外,我觉得还有优化的房间在两个层面:
- 在操作系统级别。如果操作系统知道它将页面返回到它以前属于的同一个应用程序,则无法清除它。
- CPU级别。如果CPU知道该页面曾经是空闲的,则它不能清除内存中的页面。只需在缓存中进行一些处理之后,它就可以将其清除并将其移至内存。
这里发生的事情有点复杂,因为它涉及几个不同的系统,但它肯定是而不是与上下文切换成本有关;你的程序只需要很少的系统调用(使用strace来验证)。
首先,了解约的方式malloc
实现一般工作的一些基本原则是很重要的:
- 大多数
malloc
实现在初始化过程中调用sbrk
或mmap
获得了一堆从操作系统的内存。获得的内存量可以在一些malloc
实现中进行调整。一旦获得了存储器,它通常被切割成不同的大小等级并排列成数据结构,以便当程序用例如malloc(123)
请求存储器时,实现可以快速地找到符合这些要求的存储器。 - 当您拨打
free
时,存储器返回到空闲列表,并可以在后续调用malloc
时重新使用。一些malloc
实现允许您精确调整它的工作方式。 - 当您分配大块内存时,大多数
malloc
实现将简单地将调用大量内存直接传递给mmap
系统调用,该调用一次分配“页面”的内存。对于大多数系统,1页的内存是4096字节。 - 相关的,大多数操作系统会尝试清除内存页面,然后将它们交给通过
mmap
或sbrk
请求内存的进程。这就是您在perf输出中看到拨打clear_page_orig
的原因。此功能试图将0写入内存页面。
现在,这些原则与另一个有许多名称但通常被称为“请求分页”的想法相交织。什么是“请求分页”的意思是,当用户程序从操作系统请求一块内存时(例如通过调用mmap
),内存被分配到进程的虚拟地址空间中,但是没有物理RAM支持该内存。
这里的需求寻呼过程的概述:
- 的程序调用
mmap
分配的RAM 500MB。 - 内核映射进程地址空间中所请求的500 MB RAM的地址区域。它将物理RAM的“少量”(操作系统相关)页面(通常每个4096字节)映射到这些虚拟地址。
- 用户程序通过写入来开始访问内存。
- 最终,用户程序将访问有效的地址,但没有支持它的物理RAM。
- 这会在CPU上产生页面错误。
- 内核通过查看进程正在访问有效地址来响应页面错误,但是没有物理RAM支持它。
- 然后内核找到RAM分配给该区域。如果需要将其他进程的内存写入磁盘,则可能会很慢(“换出”)。
最可能的原因,你在最后一种情况下看到一个性能下降是因为:
- 你的内核已经耗尽内存zero'd页面可分发满足您的要求对于40 MB,因此它可以一次又一次地清零内存,如你的perf输出所证明的那样。
- 为您访问没有映射尚未内存要生成页面错误。既然你正在访问40MB而不是10MB,你会为有需要的映射更多的内存页面产生更多的页面错误。
- 作为另一个答案指出,
memset
是O(n),这意味着你需要更多的内存写信时间越长。 - 的可能性较小,因为40MB没有太大的RAM这些天,但检查系统上的可用内存量只是要确保你有足够的RAM。
如果你的应用是极其性能敏感,你可以代替直接调用mmap
和:
- 通过
MAP_POPULATE
标志,这将导致所有的页面错误发生的前期和映射所有的物理内存 - 那么你将不会支付访问中的页面错误成本。 - 通过
MAP_UNINITIALIZED
标志,该标志将试图避免之前,它们分发给您的进程归零的内存页面。请注意,使用此标志是一个安全问题,除非您完全理解使用此选项的含义,否则不应使用该标志。进程可能会被发布其他不相关进程用于存储敏感信息的内存页面。还要注意你的内核必须被编译来允许这个选项。大多数内核(如AWS Linux内核)都没有默认启用此选项。您几乎可以肯定不是使用此选项。
我会提醒你,这种优化级别几乎总是一个错误;大多数应用程序的优化效果要低得多,不涉及优化页面错误代价。在实际的应用程序,我建议:
- 避免使用
memset
上的大块内存,除非它是真正必要的。大多数情况下,在通过同一过程重新使用之前调零内存不是必需的。 - 避免一遍又一遍分配和释放相同的内存块;也许你可以简单地分配一个大块,然后根据需要重新使用它。
- 使用上面如果页面故障对访问的成本确实是有害的性能(不太可能)的
MAP_POPULATE
标志。
如果您有任何问题,请留下评论,如果需要,我会很乐意编辑这篇文章。
我不确定,但我敢打赌,从用户模式切换到内核,并且再次回到上下文的成本是主宰其他的。 memset
也需要大量时间 - 记住它将是O(n)。
更新
我相信,自由并不实际可用内存的操作系统,它只是把它 的进程中,一些空闲列表。和malloc在下一次迭代 只是得到完全相同的内存块。这就是为什么没有 明显的差异。
这在原则上是正确的。经典的malloc
实现在单向链表上分配内存; free
只是简单地设置一个标志,表示不再使用分配。随着时间的推移,malloc
重新分配它第一次可以找到足够大的空闲块。这工作得不错,但可能导致碎片化。
现在有很多更好的实现,请参阅this Wikipedia article。
你没有一个具体的问题,但我看不出有什么不寻常或意外的在您的个人资料。你所做的只是用memset产生页面错误。 – user3344003
我有具体的问题。 “为什么最后一种情况比(memset for memory + memset for L3 cache)时间慢2倍”。我相信所有其他使用页面的操作都不应该太昂贵。 – homm