hom*_*omm 27 c operating-system memory-management linux-kernel
我比较熟悉虚拟内存的工作原理.所有进程内存分为页面,虚拟内存的每个页面映射到实际内存中的页面或交换文件中的页面,或者它可以是新页面,这意味着仍未分配物理页面.操作系统会根据需要将新页面映射到实际内存,而不是在应用程序请求内存时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;
}
Run Code Online (Sandbox Code Playgroud)
假设这memset只是一些内存绑定处理.在这种情况下,我们一次分配一小块内存并一次又一次地重复使用.我会像这样运行这个程序:
$ gcc -O1 ./mem.c && time ./a.out
Run Code Online (Sandbox Code Playgroud)
-O1因为需要clang用-O2完全消除了环路,并即时计算的值.
结果是:用户:0.520s,sys:0.008s.根据perf,99%的时间是memset来自libc.因此,对于这种情况,写入性能大约为20千兆字节/秒,这超过了我的内存12.5 Gb/s的理论性能.看起来这是由于L3 CPU缓存.
让我们改变测试并开始在循环中分配内存(我不会重复相同的代码部分):
#define SIZE 1 * 1024 * 1024
for (i = 0; i < 10000; i ++) {
int *p = (int *) malloc(SIZE);
memset(p, 0, SIZE);
free(p);
}
Run Code Online (Sandbox Code Playgroud)
结果完全一样.我相信free实际上并没有为操作系统释放内存,它只是把它放在进程中的一些空闲列表中.而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);
}
Run Code Online (Sandbox Code Playgroud)
时间显示:用户: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);
}
Run Code Online (Sandbox Code Playgroud)
据说perf,memset只消耗18%的时间.
显然,内存是从OS分配的,并在每一步中释放.正如我之前提到的,OS应该在使用前清除每个分配的页面.所以27.3%clear_page_orig看起来并不特别:对于清晰的mem来说它只有4s*0.273≈1.1sec - 我们在第三个例子中得到的结果相同.memset占17.9%,这导致≈700毫秒,这是正常的,因为在clear_page_orig第一和第二个例子之后已经存在于L3缓存中的存储器.
我无法理解 - 为什么最后一种情况比memset内存+ memsetL3缓存慢2倍?我可以用它做点什么吗?
结果在本机Mac OS,Vmware下的Ubuntu和Amazon c4.large实例上是可重现的(差异很小).
此外,我认为在两个层面上有优化空间:
Joe*_*ato 27
这里发生的事情是有点复杂,因为它涉及到几个不同的系统,但它肯定是不相关的上下文切换成本; 你的程序只进行很少的系统调用(通过使用strace验证这一点).
首先,了解有关实现方式malloc通常工作方式的一些基本原则非常重要:
malloc实现通过调用sbrk或mmap在初始化期间从OS获得一堆内存.在一些malloc实现中可以调整所获得的存储量.一旦获得存储器,通常将其切割成不同的大小类并且以数据结构排列,使得当程序例如请求存储器时malloc(123),malloc实现可以快速找到匹配这些要求的存储器.free,内存将返回到空闲列表,并可在后续调用时重新使用malloc.某些malloc实现允许您精确调整其工作方式.malloc实现只会将大量内存的mmap调用直接传递给系统调用,系统调用会在时间上分配内存的"页面".对于大多数系统,1页内存为4096字节.mmap或请求内存的进程sbrk.这就是你clear_page_orig在perf输出中看到调用的原因.此函数正在尝试将0写入内存页面.现在,这些原则与另一个有很多名称的想法相交,但通常被称为"请求分页"."请求分页"意味着当用户程序从OS请求一块内存(例如通过调用mmap)时,内存被分配在进程的虚拟地址空间中,但是还没有物理RAM支持该内存.
以下是需求调页过程的概述:
mmap来分配500MB的RAM.您在最后一个案例中看到性能下降的最可能原因是:
memsetO(n)意味着您需要写入的内存越多,所需的时间就越长.如果您的应用程序对性能非常敏感,则可以mmap直接调用:
MAP_POPULATE标志,这将导致所有页面错误发生在前面并映射所有物理内存 - 然后你将不会支付访问页面错误的成本.MAP_UNINITIALIZED标志,该标志将在将内容分发到您的进程之前尝试避免将内存页置零.请注意,使用此标志是一个安全问题,除非您完全理解使用此选项的含义,否则不应使用此标志.可能会发出进程的内存页面,这些页面被其他不相关的进程用于存储敏感信息.另请注意,必须编译内核以允许此选项.大多数内核(如AWS Linux内核)默认情况下不启用此选项.您几乎肯定不会使用此选项.我会提醒你,这种优化水平几乎总是一个错误; 对于优化而言,大多数应用程序具有低得多的悬挂效果,而不涉及优化页面错误成本.在现实世界的应用程序中,我建议:
memset除非真的有必要,否则不要使用大块内存.大多数情况下,在通过相同过程重新使用之前将存储器归零是不必要的.MAP_POPULATE上面的标志,如果访问页面的成本错误确实对性能有害(不太可能).如果您有任何疑问,请留下评论,我很乐意编辑这篇文章,如果需要,可以稍微扩展一下.
| 归档时间: |
|
| 查看次数: |
4581 次 |
| 最近记录: |