为什么流程的内存分配速度慢而且速度更快?

时间:2016-10-09 19:56:59

标签: c memory-management linux-kernel operating-system

我比较熟悉虚拟内存的工作原理。所有进程内存分为页面,虚拟内存的每个页面映射到实际内存中的页面或交换文件中的页面,或者它可以是新页面,这意味着仍未分配物理页面。操作系统会根据需要将新页面映射到实际内存,而不是在应用程序使用malloc请求内存时,而是仅在应用程序实际访问分配的内存中的每个页面时。但我仍有疑问。

在使用linux perf工具分析我的应用时,我注意到了这一点。

enter image description here

大约有20%的时间用于内核函数:clear_page_orig__do_page_faultget_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%的时间。

enter image description here

显然,内存是从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上,结果是可重现的(差异很小)。

另外,我认为在两个层面上有优化空间:

  • 在操作系统级别。如果操作系统知道它将页面返回到它之前所属的同一个应用程序,则无法清除它。
  • 关于CPU级别。如果CPU知道该页面曾经是空闲的,则无法清除内存中的页面。只需在缓存中进行一些处理后,它就可以在缓存中清除它并将其移动到内存中。

2 个答案:

答案 0 :(得分:25)

这里发生的事情有点复杂,因为它涉及几个不同的系统,但肯定与上下文切换成本相关;您的程序只进行很少的系统调用(使用strace验证这一点。)

首先了解一些有关malloc实施方式的基本原则非常重要:

  1. 大多数malloc实现通过在初始化期间调用sbrkmmap从操作系统获取一堆内存。获得的内存量可以在某些malloc实现中进行调整。一旦获得存储器,通常将其切割成不同的大小类并以数据结构排列,以便当程序请求例如malloc(123)的存储器时,malloc实现可以快速找到一块存储器符合这些要求。
  2. 当您致电free时,内存将返回到空闲列表,并可在后续malloc调用时重复使用。一些malloc实现允许您精确调整其工作方式。
  3. 当你分配大块内存时,大多数malloc实现只会将大量内存的调用直接传递给mmap系统调用,后者会分配&#34;页面&#34;当时的记忆。对于大多数系统,1页内存为4096字节。
  4. 相关的,大多数操作系统会尝试清除内存页面,然后再将其发送给通过mmapsbrk请求内存的进程。这就是你在perf输出中看到对clear_page_orig的调用的原因。此函数正在尝试将0写入内存页面。
  5. 现在,这些原则与另一个有很多名称的想法相交,但通常被称为&#34;请求分页。&#34;什么&#34;要求分页&#34;意味着当用户程序从OS请求一块内存时(例如通过调用mmap),内存被分配在进程的虚拟地址空间中,但是还没有物理RAM支持该内存。

    这是需求分页过程的概述:

    1. 一个名为mmap的程序,用于分配500MB的RAM。
    2. 内核映射过程中的地址区域&#39;请求的500 MB RAM的地址空间。它映射了一些&#34;少数&#34; (OS依赖)页面(通常每个4096字节)物理RAM来支持这些虚拟地址。
    3. 用户程序通过写入来开始访问内存。
    4. 最终,用户程序将访问一个有效的地址,但没有物理RAM支持它。
    5. 这会在CPU上产生页面错误。
    6. 内核通过查看进程正在访问有效地址来响应页面错误,但没有物理RAM支持它。
    7. 然后内核找到要分配给该区域的RAM。如果需要将其他进程的内存写入磁盘,这可能会很慢(&#34;换出&#34;)。
    8. 您在最后一个案例中看到性能下降的最可能原因是:

      1. 你的内核已经耗尽内存的零页面,可以分发以满足你的40 MB请求,因此它可以反复归零内存,这可以通过你的输出来证明。
      2. 当您访问尚未映射的内存时,您正在生成页面故障。由于您正在访问40mb而不是10mb,因此需要映射更多内存页面,因此会产生更多页面错误。
      3. 正如另一个答案所指出的那样,memset是O(n)意味着你需要写的内存越多,所需的时间就越长。
      4. 不太可能,因为现在40mb的RAM不多,但检查系统上的可用内存量是为了确保你有足够的RAM。
      5. 如果您的应用对性能非常敏感,则可以直接致电mmap并:

        1. 传递MAP_POPULATE标志,这将导致所有页面错误发生在前面并映射所有物理内存 - 然后您不会支付访问页面错误的成本。
        2. 传递MAP_UNINITIALIZED标志,该标志会在将内容分发到您的进程之前尝试避免将内存页置零。请注意,使用此标志是一个安全问题,除非您完全理解使用此选项的含义,否则不应使用此标志。可能会发出进程的内存页面,这些页面被其他不相关的进程用于存储敏感信息。另请注意,必须编译内核以允许此选项。大多数内核(如AWS Linux内核)默认情况下不启用此选项。您几乎肯定使用此选项。
        3. 我会提醒你,这种优化水平几乎总是一个错误;对于优化而言,大多数应用程序具有低得多的悬挂效果,而不涉及优化页面错误成本。在现实世界的应用程序中,我建议:

          1. 避免在大块内存上使用memset,除非确实有必要。大多数情况下,不需要在重复使用之前将内存清零。
          2. 避免一遍又一遍地分配和释放相同的内存块;也许您可以直接分配一个大块,然后根据需要重新使用它。
          3. 如果访问中的页面成本错误确实对性能有害(不太可能),请使用上面的MAP_POPULATE标志。
          4. 如果您有任何问题,请发表评论,我很乐意编辑此帖子,如果需要,请稍微扩展一下。

答案 1 :(得分:6)

我不确定,但我愿意打赌从用户模式到内核的上下文切换成本,然后再回到其他任何事情。 memset也需要很长时间 - 请记住它将是O(n)。

<强>更新

  

我相信free实际上并没有为操作系统释放内存,它只是放了   它在流程中的一些免费列表中。并在下一次迭代中使用malloc   只是得到完全相同的内存块。这就是为什么没有   显着的差异。

原则上,这是正确的。经典malloc实现在单链表上分配内存; free只是设置一个标志,表示不再使用分配。随着时间的推移,malloc在第一次找到足够大的空闲块时重新分配。这种方法效果很好,但可能导致碎片化。

现在有许多更流畅的实现,请参阅this Wikipedia article