gpt4 book ai didi

c - 为什么进程的内存分配很慢,可以更快吗?

转载 作者:太空狗 更新时间:2023-10-29 16:23:49 27 4
gpt4 key购买 nike

我比较熟悉虚拟内存的工作原理。所有进程内存都被划分为页面,虚拟内存的每一页都映射到实内存中的一个页面或交换文件中的一个页面,或者它可以是一个新页面,这意味着物理页面仍未分配。操作系统按需将新页面映射到实内存,而不是在应用程序通过 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只是一些内存绑定(bind)处理。在这种情况下,我们一次分配一小块内存并一次又一次地重用它。我会像这样运行这个程序:
$ gcc -O1 ./mem.c && time ./a.out
-O1需要,因为 clang-O2完全消除循环并立即计算值。

结果是:用户:0.520s,系统:0.008s。根据 perf , 99% 的时间都在 memset来自 libc .因此,对于这种情况,写入性能约为 20 GB/s,比我的内存的理论性能 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);
}

结果完全一样。我相信 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);
}

时间显示:用户:1.184s,系统:0.004s。 perf仍然报告超过 99% 的时间在 memset ,但吞吐量约为 8.3 Gb/s。那时,我或多或少地了解正在发生的事情。

如果我们继续增加内存块大小,在某些时候(对我来说是 35 Mb)执行时间将急剧增加:用户:0.724 秒,系统:3.300 秒。
#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% 的时间。

enter image description here

显然,内存是从操作系统分配的,并在每一步释放。正如我之前提到的,操作系统应该在使用前清除每个分配的页面。所以 clear_page_orig 的 27.3%看起来并不特别:clear mem 只需要 4s * 0.273 ≈ 1.1 秒——与我们在第三个例子中得到的一样。 memset占用了 17.9%,这导致 ≈ 700 毫秒,这是正常的,因为在 clear_page_orig 之后内存已经在 L3 缓存中了。 (第一个和第二个例子)。

我不明白——为什么最后一种情况比 memset 慢 2 倍用于内存 + memset L3缓存?我可以用它做点什么吗?

结果在 native Mac OS、Vmware 下的 Ubuntu 和 Amazon c4.large 实例上是可重现的(差异很小)。

另外,我认为有两个层面的优化空间:
  • 在操作系统级别 .如果操作系统知道它将页面返回给它之前所属的同一应用程序,则无法清除它。
  • 在 CPU 级别 .如果 CPU 知道该页面曾经是空闲的,则它可以不清除内存中的页面。它可以在缓存中清除它并在缓存中进行一些处理后将其移动到内存中。
  • 最佳答案

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

    首先了解一些关于方式的基本原则很重要malloc实现通常有效:

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

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

    以下是需求分页流程的概要:
  • 一个名为 mmap 的程序分配 500MB 的 RAM。
  • 内核为请求的 500 MB RAM 映射进程地址空间中的地址区域。它映射物理 RAM 的“几个”(依赖于操作系统的)页面(通常每个 4096 字节)以支持这些虚拟地址。
  • 用户程序通过写入开始访问内存。
  • 最终,用户程序将访问一个有效的地址,但没有物理 RAM 支持它。
  • 这会在 CPU 上产生页面错误。
  • 内核通过查看进程正在访问一个有效地址来响应页面错误,但没有物理 RAM 支持它。
  • 然后内核找到要分配给该区域的 RAM。如果其他进程的内存需要首先写入磁盘(“换出”),这可能会很慢。

  • 您在最后一种情况下看到性能下降的最可能原因是:
  • 您的内核已经用完了可以分配来满足您对 40 MB 的请求的零页内存,因此正如您的 perf 输出所证明的那样,它一遍又一遍地将内存归零。
  • 当您访问尚未映射的内存时,您正在生成页面错误。由于您访问的是 40mb 而不是 10mb,您将产生更多页面错误,因为需要映射的内存页面更多。
  • 正如另一个答案指出的那样,memset是 O(n) 意味着您需要写入的内存越多,所需的时间就越长。
  • 不太可能,因为现在 40mb 的 RAM 并不多,但请检查系统上的可用内存量,以确保您有足够的 RAM。

  • 如果您的应用程序对性能极其敏感,您可以改为拨打 mmap直接和:
  • 通过MAP_POPULATE标志将导致所有页面错误预先发生并将所有物理内存映射到 - 然后您将不会为访问页面错误支付成本。
  • 通过MAP_UNINITIALIZED标志,它将尝试避免在将内存页面分发到您的进程之前将其归零。请注意,使用此标志是一个安全问题,除非您完全了解使用此选项的含义,否则不应使用。进程可能会被分配给其他无关进程用于存储敏感信息的内存页。另请注意,必须编译您的内核以允许此选项。大多数内核(如 AWS Linux 内核)默认未启用此选项。您几乎肯定不应该使用此选项。

  • 我要提醒您的是,这种级别的优化几乎总是一个错误;大多数应用程序的优化悬而未决,不涉及优化缺页错误成本。在现实世界的应用程序中,我建议:
  • 避免使用 memset在大内存块上,除非确实有必要。大多数情况下,在同一进程重新使用之前清零内存是没有必要的。
  • 避免一遍又一遍地分配和释放相同的内存块;也许您可以简单地预先分配一个大块,然后根据需要重新使用它。
  • 使用 MAP_POPULATE如果访问页面错误的成本确实对性能有害(不太可能),则上面的标志。

  • 如果您有任何问题,请发表评论,如果需要,我很乐意编辑这篇文章并对此进行扩展。

    关于c - 为什么进程的内存分配很慢,可以更快吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/39947921/

    27 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com