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来测量内存使用

Embedding Python in C/C++

将Python嵌入C/C++

首先嵌入和扩展是相关联但不同的两个概念:嵌入是指在C语言中使用Python,而扩展是在Python中使用C语言(以library的形式)。

其次嵌入Python的本质是嵌入Python 解释器(Interpretor)。因此我们需要调用相应的Initialize, Finalize函数。另外,为了让C/C++识别Python相关的函数,我们还需要#include 以及相应的linker options:-Xlinker -export-dynamic -ldl -lrt -lutil

另外,Python语句有两种:statement 和 expression。注意statement是没有返回值的。因此Python语言里有exec和eval分别对应这两种情形。最本质的区别是statement有副作用(side effect),比如会把值绑定到一个名称上,比如: a = 3 。当我们用PyRun_SimpleString(“a=3”)时,这种副作用是在当前的environment下(内部实现是dict)多出一个变量(dict的key),名称是a。

此外要注意Python的执行代码是和environment相关的,比如global和local,想用的函数名称(例如:dir,str,print),变量都是保存在各自的environment里。平时我们写的 if name == "__main__" 就是说默认的环境是在模块__main__里面。我们要取出默认的函数或者变量(Python内部不严格区分这两个概念,知识函数可以callable),可以用下面的代码(以取出dir函数为例):

    PyObject* main_module =
        PyImport_AddModule("__main__");

// Get the main module's dictionary                                                                                                                                                  
// and make a copy of it.                                                                                                                                                            
    PyObject* main_dict =
        PyModule_GetDict(main_module);
    //  pFunc is also a borrowed reference 
    PyObject* pFunc = PyDict_GetItemString(pDict, "dir");

嵌入Python还应注意内存的使用。因为Python主要使用(另一种是Python的malloc, free)Reference count方式来管理新的变量,我们应记住在获得一个PyObject*类型的指针之后,用Py_DECREF或者Py_XDECREF来减少reference count (注意,特殊情况下取出的结果是不能减少reference count的,比如取出list中某个元素)。

最后给一个例子来说明怎么在Python里计算任何表达式expression (参考了FAQ[1])

#include <Python.h>
                                                                                                                                                                                     
double checkExpression(const char* formular, double gq, double dp) {
    Py_Initialize();

    // Get a reference to the main module.                                                                                                                                               
    PyObject* main_module =
        PyImport_AddModule("__main__");

    // Get the main module's dictionary                                                                                                                                                  
    // and make a copy of it.                                                                                                                                                            
    PyObject* main_dict =
        PyModule_GetDict(main_module);

    char s[1024];
    sprintf(s, "GQ=%lf", gq); //, dp, formular);                                                                                                                                     
    if ( 0 != PyRun_SimpleString(s) ) { // something wrong happen!                                                                                                                   
        fprintf(stderr, "\nSomething wrong in assigning GQ\n");
        return -1.;
    }
    sprintf(s, "DP=%lf", dp);
    if ( 0 != PyRun_SimpleString(s) ) { // something wrong happen!                                                                                                                   
        fprintf(stderr, "\nSomething wrong in assigning DP\n");
        return -1.;
    }

    PyObject* ret = PyRun_String(formular, Py_eval_input, main_dict, main_dict);
    if (ret == NULL) {
        Py_XDECREF(ret);
        PyErr_Clear();
        return -1.;
    };
    double res;
    if (PyInt_Check(ret)) {
        res = PyLong_AsLong(ret);
    } else if (PyFloat_Check(ret)) {
        res = PyFloat_AS_DOUBLE(ret);
    } else if (PyBool_Check(ret)) {
        res = ret == Py_True;
    }
    Py_XDECREF(ret);
    Py_Finalize();
    return res;
};

重要参考资料

【1】 扩展/嵌入Python的FAQ Extending/Embedding FAQ

【2】API 手册 Python/C API Reference Manual

【3】嵌入Python的流程性说明 Embedding Python in Another Application

【4】扩展Python的流程性说明,这里介绍了Python底层的知识,这些知识不会在”嵌入Python的流程性说明”中重复出现 Extending Python with C or C++