为什么进程的内存分配速度慢,速度可能会更快?

问题描述:

我比较熟悉虚拟内存是如何工作的。所有进程内存被分成页面,虚拟内存的每一页都映射到实内存中的页面或交换文件中的页面,或者它可以是新页面,这意味着物理页面仍未分配。 OS根据需要将新页面映射到实际内存,而不是当应用程序要求使用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完全消除了环路和即时计算的值。

结果是:user:0.520s,sys:0.008s。根据perf,这段时间的99%在memsetlibc。因此,对于这种情况,写入性能大约为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); 
} 

根据perfmemset将消耗只有18%的时间。

enter image description here

显然,存储器从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知道该页面曾经是空闲的,则它不能清除内存中的页面。只需在缓存中进行一些处理之后,它就可以将其清除并将其移至内存。
+0

你没有一个具体的问题,但我看不出有什么不寻常或意外的在您的个人资料。你所做的只是用memset产生页面错误。 – user3344003

+1

我有具体的问题。 “为什么最后一种情况比(memset for memory + memset for L3 cache)时间慢2倍”。我相信所有其他使用页面的操作都不应该太昂贵。 – homm

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

首先,了解约的方式malloc实现一般工作的一些基本原则是很重要的:

  1. 大多数malloc实现在初始化过程中调用sbrkmmap获得了一堆从操作系统的内存。获得的内存量可以在一些malloc实现中进行调整。一旦获得了存储器,它通常被切割成不同的大小等级并排列成数据结构,以便当程序用例如malloc(123)请求存储器时,实现可以快速地找到符合这些要求的存储器。
  2. 当您拨打free时,存储器返回到空闲列表,并可以在后续调用malloc时重新使用。一些malloc实现允许您精确调整它的工作方式。
  3. 当您分配大块内存时,大多数malloc实现将简单地将调用大量内存直接传递给mmap系统调用,该调用一次分配“页面”的内存。对于大多数系统,1页的内存是4096字节。
  4. 相关的,大多数操作系统会尝试清除内存页面,然后将它们交给通过mmapsbrk请求内存的进程。这就是您在perf输出中看到拨打clear_page_orig的原因。此功能试图将0写入内存页面。

现在,这些原则与另一个有许多名称但通常被称为“请求分页”的想法相交织。什么是“请求分页”的意思是,当用户程序从操作系统请求一块内存时(例如通过调用mmap),内存被分配到进程的虚拟地址空间中,但是没有物理RAM支持该内存。

这里的需求寻呼过程的概述:

  1. 的程序调用mmap分配的RAM 500MB。
  2. 内核映射进程地址空间中所请求的500 MB RAM的地址区域。它将物理RAM的“少量”(操作系统相关)页面(通常每个4096字节)映射到这些虚拟地址。
  3. 用户程序通过写入来开始访问内存。
  4. 最终,用户程序将访问有效的地址,但没有支持它的物理RAM。
  5. 这会在CPU上产生页面错误。
  6. 内核通过查看进程正在访问有效地址来响应页面错误,但是没有物理RAM支持它。
  7. 然后内核找到RAM分配给该区域。如果需要将其他进程的内存写入磁盘,则可能会很慢(“换出”)。

最可能的原因,你在最后一种情况下看到一个性能下降是因为:

  1. 你的内核已经耗尽内存zero'd页面可分发满足您的要求对于40 MB,因此它可以一次又一次地清零内存,如你的perf输出所证明的那样。
  2. 为您访问没有映射尚未内存要生成页面错误。既然你正在访问40MB而不是10MB,你会为有需要的映射更多的内存页面产生更多的页面错误。
  3. 作为另一个答案指出,memset是O(n),这意味着你需要更多的内存写信时间越长。
  4. 的可能性较小,因为40MB没有太大的RAM这些天,但检查系统上的可用内存量只是要确保你有足够的RAM。

如果你的应用是极其性能敏感,你可以代替直接调用mmap和:

  1. 通过MAP_POPULATE标志,这将导致所有的页面错误发生的前期和映射所有的物理内存 - 那么你将不会支付访问中的页面错误成本。
  2. 通过MAP_UNINITIALIZED标志,该标志将试图避免之前,它们分发给您的进程归零的内存页面。请注意,使用此标志是一个安全问题,除非您完全理解使用此选项的含义,否则不应使用该标志。进程可能会被发布其他不相关进程用于存储敏感信息的内存页面。还要注意你的内核必须被编译来允许这个选项。大多数内核(如AWS Linux内核)都没有默认启用此选项。您几乎可以肯定不是使用此选项。

我会提醒你,这种优化级别几乎总是一个错误;大多数应用程序的优化效果要低得多,不涉及优化页面错误代价。在实际的应用程序,我建议:

  1. 避免使用memset上的大块内存,除非它是真正必要的。大多数情况下,在通过同一过程重新使用之前调零内存不是必需的。
  2. 避免一遍又一遍分配和释放相同的内存块;也许你可以简单地分配一个大块,然后根据需要重新使用它。
  3. 使用上面如果页面故障对访问的成本确实是有害的性能(不太可能)的MAP_POPULATE标志。

如果您有任何问题,请留下评论,如果需要,我会很乐意编辑这篇文章。

我不确定,但我敢打赌,从用户模式切换到内核,并且再次回到上下文的成本是主宰其他的。 memset也需要大量时间 - 记住它将是O(n)。

更新

我相信,自由并不实际可用内存的操作系统,它只是把它 的进程中,一些空闲列表。和malloc在下一次迭代 只是得到完全相同的内存块。这就是为什么没有 明显的差异。

这在原则上是正确的。经典的malloc实现在单向链表上分配内存; free只是简单地设置一个标志,表示不再使用分配。随着时间的推移,malloc重新分配它第一次可以找到足够大的空闲块。这工作得不错,但可能导致碎片化。

现在有很多更好的实现,请参阅this Wikipedia article