我比较熟悉虚拟内存的工作原理。所有进程内存分为页面,虚拟内存的每个页面映射到实际内存中的页面或交换文件中的页面,或者它可以是新页面,这意味着仍未分配物理页面。操作系统会根据需要将新页面映射到实际内存,而不是在应用程序使用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
完全取消了循环并立即计算了值。
结果是:用户:0.520s,sys:0.008s。根据{{1}},99%的时间都来自perf
memset
。因此,对于这种情况,写入性能大约为20千兆字节/秒,这超过了我的内存12.5 Gb / s的理论性能。看起来这是由于L3 CPU缓存。
让我们改变测试并开始在循环中分配内存(我不会重复相同的代码部分):
libc
结果完全一样。我相信#define SIZE 1 * 1024 * 1024
for (i = 0; i < 10000; i ++) {
int *p = (int *) malloc(SIZE);
memset(p, 0, SIZE);
free(p);
}
实际上并没有为操作系统释放内存,它只是把它放在进程中的一些空闲列表中。并且在下一次迭代中free
只获得完全相同的内存块。这就是为什么没有明显的区别。
让我们从1兆字节开始增加SIZE。执行时间会逐渐增加,并且会在10兆字节附近饱和(10到20兆字节之间没有差别)。
malloc
时间显示:用户:1.184s,sys:0.004s。 #define SIZE 10 * 1024 * 1024
for (i = 0; i < 1000; i ++) {
int *p = (int *) malloc(SIZE);
memset(p, 0, SIZE);
free(p);
}
仍然报告99%以上的时间都在perf
,但吞吐量约为8.3 Gb / s。那时,我或多或少地了解发生了什么。
如果我们将继续增加内存块大小,在某些时候(对于我35 Mb),执行时间将急剧增加:用户:0.724s,sys:3.300s。
memset
根据#define SIZE 40 * 1024 * 1024
for (i = 0; i < 250; i ++) {
int *p = (int *) malloc(SIZE);
memset(p, 0, SIZE);
free(p);
}
,perf
只会消耗18%的时间。
显然,内存是从OS分配的,并在每一步都被释放。正如我之前提到的,OS应该在使用前清除每个分配的页面。所以27.3%的memset
看起来并不特别:对于清晰的mem来说它只有4s *0.273≈1.1秒 - 我们在第三个例子中也是如此。 clear_page_orig
占17.9%,这导致≈700毫秒,这是正常的,因为memset
之后内存已经存在于L3缓存中(第一和第二个例子)。
我无法理解 - 为什么最后一种情况比内存clear_page_orig
慢2倍,而对于L3缓存只有memset
?我可以用它做点什么吗?
在本机Mac OS,Vmware和Amazon c4.large实例下的Ubuntu上,结果是可重现的(差异很小)。
另外,我认为在两个层面上有优化空间:
答案 0 :(得分:25)
这里发生的事情有点复杂,因为它涉及几个不同的系统,但肯定不与上下文切换成本相关;您的程序只进行很少的系统调用(使用strace验证这一点。)
首先了解一些有关malloc
实施方式的基本原则非常重要:
malloc
实现通过在初始化期间调用sbrk
或mmap
从操作系统获取一堆内存。获得的内存量可以在某些malloc
实现中进行调整。一旦获得存储器,通常将其切割成不同的大小类并以数据结构排列,以便当程序请求例如malloc(123)
的存储器时,malloc
实现可以快速找到一块存储器符合这些要求。free
时,内存将返回到空闲列表,并可在后续malloc
调用时重复使用。一些malloc
实现允许您精确调整其工作方式。malloc
实现只会将大量内存的调用直接传递给mmap
系统调用,后者会分配&#34;页面&#34;当时的记忆。对于大多数系统,1页内存为4096字节。mmap
或sbrk
请求内存的进程。这就是你在perf输出中看到对clear_page_orig
的调用的原因。此函数正在尝试将0写入内存页面。现在,这些原则与另一个有很多名称的想法相交,但通常被称为&#34;请求分页。&#34;什么&#34;要求分页&#34;意味着当用户程序从OS请求一块内存时(例如通过调用mmap
),内存被分配在进程的虚拟地址空间中,但是还没有物理RAM支持该内存。
这是需求分页过程的概述:
mmap
的程序,用于分配500MB的RAM。您在最后一个案例中看到性能下降的最可能原因是:
memset
是O(n)意味着你需要写的内存越多,所需的时间就越长。如果您的应用对性能非常敏感,则可以直接致电mmap
并:
MAP_POPULATE
标志,这将导致所有页面错误发生在前面并映射所有物理内存 - 然后您不会支付访问页面错误的成本。 MAP_UNINITIALIZED
标志,该标志会在将内容分发到您的进程之前尝试避免将内存页置零。请注意,使用此标志是一个安全问题,除非您完全理解使用此选项的含义,否则不应使用此标志。可能会发出进程的内存页面,这些页面被其他不相关的进程用于存储敏感信息。另请注意,必须编译内核以允许此选项。大多数内核(如AWS Linux内核)默认情况下不启用此选项。您几乎肯定不使用此选项。我会提醒你,这种优化水平几乎总是一个错误;对于优化而言,大多数应用程序具有低得多的悬挂效果,而不涉及优化页面错误成本。在现实世界的应用程序中,我建议:
memset
,除非确实有必要。大多数情况下,不需要在重复使用之前将内存清零。MAP_POPULATE
标志。如果您有任何问题,请发表评论,我很乐意编辑此帖子,如果需要,请稍微扩展一下。
答案 1 :(得分:6)
我不确定,但我愿意打赌从用户模式到内核的上下文切换成本,然后再回到其他任何事情。 memset
也需要很长时间 - 请记住它将是O(n)。
<强>更新强>
我相信free实际上并没有为操作系统释放内存,它只是放了 它在流程中的一些免费列表中。并在下一次迭代中使用malloc 只是得到完全相同的内存块。这就是为什么没有 显着的差异。
原则上,这是正确的。经典malloc
实现在单链表上分配内存; free
只是设置一个标志,表示不再使用分配。随着时间的推移,malloc
在第一次找到足够大的空闲块时重新分配。这种方法效果很好,但可能导致碎片化。
现在有许多更流畅的实现,请参阅this Wikipedia article。