在 Linux 上的 C++ 程序中分析常驻内存使用情况和许多页面错误

Posted

技术标签:

【中文标题】在 Linux 上的 C++ 程序中分析常驻内存使用情况和许多页面错误【英文标题】:Profiling resident memory usage and many page faults in C++ program on linux 【发布时间】:2020-05-11 19:29:56 【问题描述】:

我试图弄清楚为什么我的一个程序版本(“新”)的常驻内存比同一程序的另一个版本(“基线”)高得多(5x)。该程序在具有 E5-2698 v3 CPU 并用 C++ 编写的 Linux 集群上运行。基线是多进程程序,新的是多线程程序;它们基本上都在执行相同的算法、计算和对相同的输入数据进行操作等。在两者中,进程或线程的数量与内核 (64) 一样多,线程固定在 CPU 上。我已经使用 Valgrind Massif 和 Heaptrack 进行了大量的堆分析,它们表明内存分配是相同的(应该如此)。程序的基线版本和新版本的 RSS 都比 LLC 大。

这台机器有 64 个内核(超线程)。对于这两个版本,我在straced 相关过程中发现了一些有趣的结果。这是我使用的 strace 命令:

strace -k -p <pid> -e trace=mmap,munmap,brk

以下是有关这两个版本的一些详细信息:

基准版本:

64 个进程 RES 约为每个进程 13 MiB 使用大页面 (2MB) 没有从上面列出的 strace 调用进行任何与 malloc/free 相关的系统调用(更多内容见下文)

顶部输出

新版本

2 个进程 每个进程 32 个线程 RES 约为每个进程 2 GiB 使用大页面 (2MB) 此版本执行大量memcpy 调用大型缓冲区(25MB),默认设置为memcpy(我认为应该使用非临时存储,但我尚未验证这一点) 在发布和配置文件构建中,生成了许多 mmapmunmap 调用。奇怪的是,在调试模式下没有生成任何东西。 (更多内容见下文)。

顶部输出(与基线相同的列)

假设我没看错,与基线版本相比,新版本的 RSS 聚合(整个节点)高出 5 倍,并且使用 perf stat 测量的页面错误明显更多。当我对 page-faults 事件运行 perf record/report 时,它显示所有页面错误都来自程序中的 memset。但是,基线版本也具有该 memset,并且没有因此而导致的页面错误(使用 perf record -e page-faults 验证)。一种想法是,由于某种原因,还有其他一些内存压力导致 memset 出现页面错误。

那么,我的问题是如何理解驻留内存的大幅增加来自何处?是否有性能监视器计数器(即性能事件)可以帮助阐明这一点?或者,是否有类似 heaptrack 或 massif 的工具可以让我查看构成 RES 足迹的实际数据是什么?

我在闲逛时注意到的最有趣的事情之一是上面提到的 mmapmunmap 调用的不一致。基线版本没有生成任何这些;新版本的配置文件和发布版本(基本上是-march=native-O3)确实发出了这些系统调用,但新版本的调试版本没有调用mmapmunmap(超过几十秒跟踪)。请注意,应用程序基本上是分配一个数组,进行计算,然后释放该数组——所有这些都在一个运行多次的外循环中。

在某些情况下,分配器似乎能够轻松地重用先前外部循环迭代中分配的缓冲区,但在其他情况下则不然——尽管我不明白这些事情是如何工作的,也不知道如何影响它们。我相信分配器有一个时间窗口的概念,之后应用程序内存将返回给操作系统。一种猜测是,在优化的代码(发布版本)中,向量化指令用于计算,它使其速度更快。这可能会改变程序的时间,以便将内存返回给操作系统;尽管我不明白为什么基线中没有发生这种情况。也许线程正在影响这一点?

(作为一个暗中评论,我还要说我尝试了 jemalloc 分配器,无论是使用默认设置还是更改它们,但新版本的速度降低了 30%,但没有使用 jemalloc 时基线发生变化。我对此感到有点惊讶,因为我以前使用 jemalloc 的经验是,它往往会在多线程程序中产生一些加速。我添加此评论以防引发其他想法。)

【问题讨论】:

您确定基准版本没有将 malloc+memset 优化为 calloc 从而使页面保持不变?版本之间的更改是否可能让系统以不同的方式使用透明的大页面,而这种方式恰好不适合您的工作负载?或者可能只是不同的分配/空闲使您的分配器手动页面回到操作系统而不是将它们保留在空闲列表中,从而导致每次分配后出现页面错误。也许strace 用于mmap / munmapbrk 系统调用。 我认为你的问题是在正确的轨道上,虽然我不明白你说的一些事情。基线被实现为使用许多进程而没有线程;新的在同一进程中有许多线程。现在,线程版本可能会进行一些优化。我正在使用大页面(2MB),尽管我不太了解线程等如何影响它。我也会尝试 strace 。我确实看到 perf top 显示新版本在syscall_return_via_sysret 中花费时间,因此可能发生了一些系统调用。 在你的问题中加入这样的细节;这是一个巨大的变化。有一些方法可以找出哪些页面是常驻的 (mincore()),但要确定其含义取决于您所做的更改! @PeterCordes 你是对的——我添加了额外的细节并发现了一些有趣的差异,因为 strace 正如你所建议的那样。请查看更新后的帖子。 好的,这就是您的页面错误的来源。 strace 测试非常明确,munmap 调用的回溯可以识别出有罪的free 调用。要修复它,请参阅gnu.org/software/libc/manual/html_node/… / man7.org/linux/man-pages/man3/mallopt.3.html,尤其是 M_MMAP_THRESHOLD(提高它以使 glibc malloc 不为您的数组使用 mmap?)。我之前没有玩过参数。手册页提到了有关动态 mmap 阈值的内容。 【参考方案1】:

一般来说:GCC 可以将 malloc+memset 优化为 calloc,从而保持页面不变。如果您实际上只触及大分配的几页,那么没有发生可能会导致页面错误的巨大差异。

或者版本之间的变化是否可能让系统以不同的方式使用透明的大页面,而这种方式恰好不适合您的工作负载?

或者也许只是不同的分配/空闲使您的分配器手页回到操作系统,而不是将它们保留在空闲列表中。延迟分配意味着您在从内核获取页面后第一次访问页面时会遇到软页面错误。 strace 查找 mmap / munmapbrk 系统调用。


在您的具体情况下,您的strace 测试确认您的更改导致malloc / free 将页面交还给操作系统,而不是保留在空闲列表中。

这完全解释了额外的页面错误。 munmap 调用的回溯可以识别有罪的免费调用。要修复它,请参阅 https://www.gnu.org/software/libc/manual/html_node/Memory-Allocation-Tunables.html / http://man7.org/linux/man-pages/man3/mallopt.3.html,尤其是 M_MMAP_THRESHOLD(也许提高它以使 glibc malloc 不使用 mmap 为您的数组?)。我之前没有玩过参数。手册页提到了有关动态 mmap 阈值的内容。


它没有解释额外的 RSS;你确定你不是不小心分配了 5 倍的空间吗?如果你不是,也许更好的分配对齐可以让内核使用以前没有的透明大页,可能导致在数组末尾浪费多达 1.99 MiB 而不是不到 4k?或者,如果您只分配超过 2M 边界的前几个 4k 页面,Linux 可能不会使用大页面。

如果您在 memset 中遇到页面错误,我假设这些数组不是稀疏的,并且您正在触摸每个元素。


我相信分配器有一个时间窗口的概念,在此之后应用程序内存返回给操作系统

分配器可能在您每次调用free 时检查当前时间,但这很昂贵,因此不太可能。他们也不太可能使用信号处理程序或单独的线程来定期检查空闲列表大小。

我认为 glibc 只是使用基于大小的启发式算法,它对每个 free 进行评估。正如我所说,手册页提到了一些关于启发式的内容。

IMO 实际调整更适合您情况的 malloc(或找到不同的 malloc 实现)可能是另一个问题。

【讨论】:

谢谢 - 这太棒了。尽管M_MMAP_THRESHOLD 是我将在必要时使用的东西,但我能够通过应用程序更改删除额外的系统调用。 关于分配器时间窗口评论:我从 jemalloc 得到这个概念——参见github.com/jemalloc/jemalloc/blob/dev/TUNING.md,特别是dirty_decay_msmuzzy_decay_ms。不过,我还没有看到任何关于 glibc malloc 的信息。 一个未解决的问题是 RSS 开销——顶部仍然显示相同的大 RSS。我还使用github.com/brendangregg/wss (wss.pl) 来验证我的工作集大小,即使在修复了页面错误问题之后,在新版本中仍然很大。当您提到启用 THP 优化的对齐问题时——这里的最佳实践是什么?对于这些 26MB 分配,我应该使用 posix_memalign 与 2MB 对齐吗? @Kulluk007:如果您的代码重复使用所有 26MB 分配,那么是的,posix_memalignaligned_alloc 分配 2M 对齐的区域可能很好。 IDK 他们如何将此传达给 mmap,或者他们是否只是过度分配。虚拟地址空间很便宜(尤其是未触及的页面),所以很好。然后在你的分配上使用madvise(MADV_HUGE) 告诉内核更喜欢大页面。特别是如果您将/sys/kernel/mm/transparent_hugepage/defrag 设置为defer+madvise,那么madvise 会提示内核花时间进行碎片整理以获得2M 块连续的物理内存。 我已经尝试了posix_memalignmemalign 的建议,但这并没有改变新版本正在使用的常驻内存量。我想知道是否有任何直接的方法来分析常驻内存——有点像常驻内存的 valgrind massif,它映射回代码中的原始分配。鉴于mincore 和一些簿记,似乎可以构建一些工具。无论如何,我很好奇 mincore 是否是评估居民来自哪里的最佳方法,或者是否还有其他需要考虑的方法。

以上是关于在 Linux 上的 C++ 程序中分析常驻内存使用情况和许多页面错误的主要内容,如果未能解决你的问题,请参考以下文章

如何在 C++ 中分析和捕获双重删除和内存损坏

使用 Valgrind 在 Python 程序中分析内存时遇到问题

如何在 C 中分析 openMPI 程序的内存使用情况和性能

在 PyCharm 中分析 python 时内存使用率非常高

在 Ruby on Rails 应用程序/内存泄漏中分析延迟作业任务

如何从核心转储中分析内存使用情况?