在 linux 上分析 C++ 程序中的驻留内存使用情况和许多页面错误
Profiling resident memory usage and many page faults in C++ program on linux
我想弄清楚为什么我的一个程序版本 ("new") 的驻留内存比同一程序的另一个版本 ("baseline") 高得多 (5x)。该程序 运行ning 在具有 E5-2698 v3 CPU 的 Linux 集群上并用 C++ 编写。 baseline是多进程程序,新的是多线程程序;它们基本上都在执行相同的算法、计算,并对相同的输入数据进行操作等。在两者中,进程或线程的数量与核心 (64) 一样多,线程固定在 CPU 上。我已经使用 Valgrind Massif 和 Heaptrack 完成了大量的堆分析,它们表明内存分配是相同的(应该是这样)。该程序的基线和新版本的 RSS 都比 LLC 大。
机器有64个核心(超线程)。对于这两个版本,我都strace
d 了相关流程,发现了一些有趣的结果。这是我使用的 strace 命令:
strace -k -p <pid> -e trace=mmap,munmap,brk
以下是关于这两个版本的一些细节:
基准版本:
- 64 个进程
- RES 约为每个进程 13 MiB
- 使用大页面 (2MB)
- 没有 malloc/free-related 系统调用是从上面列出的 strace 调用进行的(更多内容见下文)
最高输出
新版本
- 2 个进程
- 每个进程 32 个线程
- RES 大约是每个进程 2 GiB
- 使用大页面 (2MB)
- 此版本使用
memcpy
的默认设置(我认为应该使用非临时存储,但我没有'验证了这一点)
- 在发布和配置文件构建中,生成了许多
mmap
和 munmap
调用。奇怪的是,none 是在调试模式下生成的。 (更多内容见下文)。
最高输出(与基线相同的列)
假设我没看错,与基线版本相比,新版本的 RSS 总量(整个节点)高出 5 倍,并且使用 perf stat 测量的页面错误明显更多。当我在页面错误事件上 运行 perf record/report 时,它显示所有页面错误都来自程序中的 memset。然而,基线版本也有那个 memset 并且没有页面错误(使用 perf record -e page-faults
验证)。一种想法是,由于某种原因还有其他一些内存压力导致 memset 出现页面错误。
所以,我的问题是,我如何才能理解常驻内存的大幅增加来自何处?是否有性能监视器计数器(即性能事件)可以帮助阐明这一点?或者,是否有类似 heaptrack 或 massif 的工具可以让我查看构成 RES 足迹的实际数据是什么?
我在闲逛时注意到的最有趣的事情之一是上面提到的 mmap
和 munmap
调用的不一致。基线版本没有生成任何这些;新版本的配置文件和发布版本(基本上,-march=native
和 -O3
)确实发出了这些系统调用,但新版本的调试版本没有调用 mmap
和 munmap
(超过几十秒的跟踪)。请注意,应用程序基本上是 malloc 一个数组,进行计算,然后释放该数组——所有这些都在一个 运行s 多次的外循环中进行。
在某些情况下,分配器似乎能够轻松地重用前一个外循环迭代中分配的缓冲区,但在其他情况下却不能——尽管我不明白这些东西是如何工作的,也不知道如何影响它们。我相信分配器有一个时间 window 的概念,之后应用程序内存返回到 OS。一种猜测是,在优化代码(发布版本)中,矢量化指令用于计算,这使得计算速度更快。这可能会改变程序的时间,使内存返回到 OS;尽管我不明白为什么基线中没有发生这种情况。也许线程正在影响这个?
(作为暗中评论,我还要说我尝试了 jemalloc 分配器,既有默认设置也有更改它们,新版本速度降低了 30%但是使用 jemalloc 时基线没有变化。我在这里有点惊讶,因为我以前使用 jemalloc 的经验是它往往会产生一些多线程程序的加速。我添加此评论以防它引发其他一些想法。)
总的来说:GCC 可以将 malloc+memset 优化为 calloc,从而使页面保持不变。如果您实际上只接触了大型分配的几页,那么 不 的发生可能会导致页面错误的巨大差异。
或者版本之间的变化是否会让系统以不同的方式使用透明大页面,而这种方式恰好不利于您的工作负载?
或者可能只是不同的分配/空闲使您的分配器手页返回到 OS 而不是将它们保留在空闲列表中。延迟分配意味着您在从内核获取页面后第一次访问页面时会遇到软页面错误。 strace
寻找 mmap
/ munmap
或 brk
系统调用。
在您的具体情况下,您的 strace
测试确认您的更改导致 malloc
/ free
将页面返回给 OS将它们保留在免费列表中。
这充分解释了额外的页面错误。 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;您确定您没有意外分配 space 的 5 倍吗?如果你不是,也许更好的分配对齐让内核在它以前没有的地方使用透明的大页面,可能导致在数组末尾浪费高达 1.99 MiB 而不是不到 4k?或者,如果您只分配超过 2M 边界的前几个 4k 页,Linux 可能不会使用大页。
如果您在 memset
中遇到页面错误,我假设这些数组不是稀疏的并且您正在触及每个元素。
I believe allocators have a notion of a time window after which application memory is returned to the OS
分配器在您每次调用 free
时检查当前时间是 可能的 ,但这很昂贵,所以不太可能。他们也不太可能使用信号处理程序或单独的线程来定期检查空闲列表大小。
我认为 glibc 只是使用基于大小的启发式算法,它会在每个 free
上进行评估。正如我所说,手册页提到了一些关于启发式的东西。
IMO 实际调整 malloc(或寻找不同的 malloc 实现)更适合您的情况可能应该是一个不同的问题。
我想弄清楚为什么我的一个程序版本 ("new") 的驻留内存比同一程序的另一个版本 ("baseline") 高得多 (5x)。该程序 运行ning 在具有 E5-2698 v3 CPU 的 Linux 集群上并用 C++ 编写。 baseline是多进程程序,新的是多线程程序;它们基本上都在执行相同的算法、计算,并对相同的输入数据进行操作等。在两者中,进程或线程的数量与核心 (64) 一样多,线程固定在 CPU 上。我已经使用 Valgrind Massif 和 Heaptrack 完成了大量的堆分析,它们表明内存分配是相同的(应该是这样)。该程序的基线和新版本的 RSS 都比 LLC 大。
机器有64个核心(超线程)。对于这两个版本,我都strace
d 了相关流程,发现了一些有趣的结果。这是我使用的 strace 命令:
strace -k -p <pid> -e trace=mmap,munmap,brk
以下是关于这两个版本的一些细节:
基准版本:
- 64 个进程
- RES 约为每个进程 13 MiB
- 使用大页面 (2MB)
- 没有 malloc/free-related 系统调用是从上面列出的 strace 调用进行的(更多内容见下文)
最高输出
新版本
- 2 个进程
- 每个进程 32 个线程
- RES 大约是每个进程 2 GiB
- 使用大页面 (2MB)
- 此版本使用
memcpy
的默认设置(我认为应该使用非临时存储,但我没有'验证了这一点) - 在发布和配置文件构建中,生成了许多
mmap
和munmap
调用。奇怪的是,none 是在调试模式下生成的。 (更多内容见下文)。
最高输出(与基线相同的列)
假设我没看错,与基线版本相比,新版本的 RSS 总量(整个节点)高出 5 倍,并且使用 perf stat 测量的页面错误明显更多。当我在页面错误事件上 运行 perf record/report 时,它显示所有页面错误都来自程序中的 memset。然而,基线版本也有那个 memset 并且没有页面错误(使用 perf record -e page-faults
验证)。一种想法是,由于某种原因还有其他一些内存压力导致 memset 出现页面错误。
所以,我的问题是,我如何才能理解常驻内存的大幅增加来自何处?是否有性能监视器计数器(即性能事件)可以帮助阐明这一点?或者,是否有类似 heaptrack 或 massif 的工具可以让我查看构成 RES 足迹的实际数据是什么?
我在闲逛时注意到的最有趣的事情之一是上面提到的 mmap
和 munmap
调用的不一致。基线版本没有生成任何这些;新版本的配置文件和发布版本(基本上,-march=native
和 -O3
)确实发出了这些系统调用,但新版本的调试版本没有调用 mmap
和 munmap
(超过几十秒的跟踪)。请注意,应用程序基本上是 malloc 一个数组,进行计算,然后释放该数组——所有这些都在一个 运行s 多次的外循环中进行。
在某些情况下,分配器似乎能够轻松地重用前一个外循环迭代中分配的缓冲区,但在其他情况下却不能——尽管我不明白这些东西是如何工作的,也不知道如何影响它们。我相信分配器有一个时间 window 的概念,之后应用程序内存返回到 OS。一种猜测是,在优化代码(发布版本)中,矢量化指令用于计算,这使得计算速度更快。这可能会改变程序的时间,使内存返回到 OS;尽管我不明白为什么基线中没有发生这种情况。也许线程正在影响这个?
(作为暗中评论,我还要说我尝试了 jemalloc 分配器,既有默认设置也有更改它们,新版本速度降低了 30%但是使用 jemalloc 时基线没有变化。我在这里有点惊讶,因为我以前使用 jemalloc 的经验是它往往会产生一些多线程程序的加速。我添加此评论以防它引发其他一些想法。)
总的来说:GCC 可以将 malloc+memset 优化为 calloc,从而使页面保持不变。如果您实际上只接触了大型分配的几页,那么 不 的发生可能会导致页面错误的巨大差异。
或者版本之间的变化是否会让系统以不同的方式使用透明大页面,而这种方式恰好不利于您的工作负载?
或者可能只是不同的分配/空闲使您的分配器手页返回到 OS 而不是将它们保留在空闲列表中。延迟分配意味着您在从内核获取页面后第一次访问页面时会遇到软页面错误。 strace
寻找 mmap
/ munmap
或 brk
系统调用。
在您的具体情况下,您的 strace
测试确认您的更改导致 malloc
/ free
将页面返回给 OS将它们保留在免费列表中。
这充分解释了额外的页面错误。 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;您确定您没有意外分配 space 的 5 倍吗?如果你不是,也许更好的分配对齐让内核在它以前没有的地方使用透明的大页面,可能导致在数组末尾浪费高达 1.99 MiB 而不是不到 4k?或者,如果您只分配超过 2M 边界的前几个 4k 页,Linux 可能不会使用大页。
如果您在 memset
中遇到页面错误,我假设这些数组不是稀疏的并且您正在触及每个元素。
I believe allocators have a notion of a time window after which application memory is returned to the OS
分配器在您每次调用 free
时检查当前时间是 可能的 ,但这很昂贵,所以不太可能。他们也不太可能使用信号处理程序或单独的线程来定期检查空闲列表大小。
我认为 glibc 只是使用基于大小的启发式算法,它会在每个 free
上进行评估。正如我所说,手册页提到了一些关于启发式的东西。
IMO 实际调整 malloc(或寻找不同的 malloc 实现)更适合您的情况可能应该是一个不同的问题。