Linux环境下测量程序的内存占用

Measure memory consumption of programs in Linux

在衡量程序性能时不仅需要测量运行时间,还常常需要测量其内存使用。比如如果一个程序比更一个程序快,有可能是以空间换取时间,因此单纯比较运行速度就失去了意义。我们这里演示如何在Linux环境下测量程序的内存占用。

我们先要弄清楚内存的含义:对于运行中的程序而言,它有四个部分:数据,文本,堆,栈(data, text, stack, heap)。对于操作系统而言,所有分配给程序的内存都用虚拟内存表示(Virtual Memory)。对硬件而言,内存分为物理内存(Physical)和交换内存(Swap)。这三种环境的关系有两种:(1)程序内存会被映射到虚拟内存,即虚拟内存中分段对应数据、文本、堆、栈; (2)虚拟内存中某些部分放在物理内存中,另一些放到交换内存中。

为了程序运行有空间上的效率,Linux系统下会共享内存。例如共享程序库(比如libc)或者是内存映射(memory map)。据我的理解,共享部分在程序空间中可以是文本部分,在虚拟内存中放在相同的地址,在硬件中可放在物理内存或交换内存。

对应上面不同的概念,Linux常用的内存相关术语有:
常驻内存(Resident memory), 表示硬件中物理内存的占用。
虚拟内存,Virtual memory,操作系统使用内存的一种抽象。
页(page),操作系统以页为单位管理(分配,回收)内存

测量程序的内存占用可以用不同的统计量。从程序的生命周期来看,可以分为最大(peak)内存占用,即时内存占用,和平均(average)内存占用。从内存的含义来讲,可以在程序本身的语境(context),或是操作系统,或是硬件的语境,比如:栈内存使用(heap memory),虚拟内存占用,物理内存占用。

测量一个程序的内存占用时,最理想的是我们知道(每时每刻)程序的四个部分(在虚拟内存中)分别有多大,对应的,在物理内存或者交换内存中占用了多少空间。但是这是一个很难达到的目标,因为:(1)操作系统以页方式分配内存,很难得到每一个页中具体多少空间被占用;(2)虚拟内存往往比物理内存大很多,当操作系统给一个运行的程序分配内存时总是提供多余程序精确需要的内存;(3)注程序在物理内存的占用加上程序在交换内存的占用构成了程序总的使用情况,但是现有的工具并不直接给出这一数值,需要手动把Resident Memory和Shared Memory相加 。

在实际情况下,通过一些现有的工具,我们往往更关心这几个易于获得统计量:
(1)max resident memory size
可以使用time -v命令(要指定全部路径,否则bash的time命令不识别-v)
注意这个测量值往往远远高于程序运行时消耗的内存。
(这是time 1.7版本的bug,time 报告的内存用量是实际的4倍,因为wait3/wait4返回的实际内存用量,单位是kbyte,但time错把单位设定为page)
本质上time使用了wait3或者wait4命令来获取程序的resources (见 ‘man wait3’)。
使用Python实现时,可以用resources module来获取。
另外,使用这个思路还可以得到程序的运行时间(用户时间,内核时间,实际时间)

(2)top 或者ps
可以查看程序的Resident Size(RES)列,Share Memory(SHR)以及Virtual Memory Size(VIRT),这一统计量默认每秒更新一次。这一方式的缺点是测量值为即时测量,每时每刻这个值都有可能变化。优点是使用起来很方便。
使用Python实现时,可以先使用multiprocesing.Process启动一个程序,然后使用psutil module,构造psutl.Process(),然后使用get_memory_info方式来获取rss,vms

(3)与(2)类似,但为了更多的信息,还可以读取/proc
这是通过读取/proc//status和/proc//smaps来获得程序的内存使用量的。理论上这是最精确的测量。这两个文件的具体格式可以用‘man proc’来获得。
在Python中,需要另外写程序来解释这两个文件,例如下面这样:

zhanxw@amd: ~> cat /proc/15648/status
Name:	takeMem
State:	R (running)
Tgid:	15648
Pid:	15648
PPid:	15163
TracerPid:	0
Uid:	248396	248396	248396	248396
Gid:	248396	248396	248396	248396
FDSize:	256
Groups:	500 1007 1013 1017 1028 1033 248396 
VmPeak:	   12752 kB
VmSize:	   12752 kB
VmLck:	       0 kB
VmHWM:	    1756 kB
VmRSS:	    1756 kB
VmData:	    1112 kB
VmStk:	     136 kB
VmExe:	       4 kB
VmLib:	    3232 kB
VmPTE:	      40 kB
VmSwap:	       0 kB
Threads:	1

在实践中,可以使用已有的程序,例如tmem【4】,以及lh3的runit【6】(下载udp,在runit/下,用make编译)。输出如下。

tmem

./tmem ./takeMem 1000000000
	    3908		    3908	      84	      84
Allocating 1000000000 memory
	  988336		  988336	  148012	  148012
	  988336		  988336	  308524	  308524
	  988336		  988336	  442636	  442636
	  988336		  988336	  567244	  567244
	  988336		  988336	  686308	  686308
	  988336		  988336	  796396	  796396
	  988336		  988336	  889588	  889588
	  988336		  988336	  949516	  949516
runit

./runit ~/mycode/smake/takeMem 1000
Allocating 1000 memory
-- CMD: ./runit /net/fantasia/home/zhanxw/mycode/smake/takeMem 1000

-- totalmem     198345252.000 kB
-- available    190966956.000 kB
-- free           1569216.000 kB

-- retval                   0
-- real                15.749 sec
-- user                15.350 sec
-- sys                  0.170 sec
-- maxrss             704.000 kB
-- avgrss             704.000 kB
-- maxvsize         11904.000 kB
-- avgvsize         11904.000 kB

参考资料:
【1】Python resource 模块
【2】Python os 模块,介绍wait3,wait4
【3】Python multiprocessing模块,介绍了Process这个易于使用的类,与subprocess相似,但有更灵活的用法
【4】Measuring Memory Usage,介绍了用C来读取/proc//status的程序,并给出tmem的下载链接
【5】Understanding memory,非常详细的介绍Linux内存分配,优化
【6】Heng Li’s personal website 可以下载比较hash library的程序,里面有一个runint/文件夹,编译之后可以用runit来测量内存使用