当前位置: 首页 > 科技观察

Python内存分配、常驻内存与测量

时间:2023-03-15 23:09:48 科技观察

要想精通一门语言,熟悉它的内容分配和使用机制很重要。对于C、C++等编译型语言,内存的使用完全由程序员自己的代码来分配和管理,因此对C、C++程序员的内存机制非常熟悉。但是对于动态语言,比如Python,内存是在语言层自动管理的,所以程序员不需要过多关注细节,但是如果你想高效可靠地编写自己的代码,你还必须了解语言的记忆机制。本文介绍Python语言的内存机制,以及如何衡量其内存。概述考虑以下代码:importnumpyasnpcc=np.ones((1024,1024,1024,3),dtype=np.uint8)此代码将创建一个3GB字节的数组并用1填充它们。同学们,你可能期望运行这段代码后,进程会自动分配3GB内存供使用。这是真的?一种测量内存的方法是使用“常驻内存”。在Python中,可以使用psutil这个信息方便库工具查看当前进程的常驻内存:importpsutil.Process().memory_info().rss/(1024*1024)3093本例中进程使用了??3093MB或3.09GB,与预期的数组大小相同,没有差异。但是常驻内存其实并没有那么简单。假设机器上正在运行一些消耗内存的任务。然后切换回解释器并再次运行完全相同的命令:psutil.Process().memory_info().rss/(1024*1024)2903.12109375发生了什么事?减少200MB内存。为了解释这种现象,有必要了解操作系统的内存管理机制。简化模型当前运行的程序会分配一些内存,即从操作系统中获取虚拟内存中的地址。虚拟内存是特定于进程的地址空间,本质上是从0到264-1,进程可以在其中读取或写入字节。在C语言中,程序员可以使用malloc()或mmap()函数进行手动内存分配;在Python中,我们只需要创建对象,Python解释器会在底层自动调用malloc()或mmap()。然后该进程可以读取或写入该特定地址和连续字节。在Linux下,可以使用ltrace工具跟踪调用malloc(),运行如下Python代码:importnumpyasnpcc=np.ones((170_000,),dtype=np.uint8),然后运行ltrace:ltrace-emallocpythotones.py..._multiarray_umath.cpython-39-x86_64-linux-gnu.so->malloc(170000)=0x5638862a45e0...整个过程Python创建一个NumPy数组。在Python引擎NumPy中调用malloc()。malloc()的结果是内存中的地址:0x5638862a45e0。实现NumPy的C代码然后可以读取和写入该地址和下一个连续的169,999地址,每个地址代表虚拟内存中的一个字节。那170,000字节存储在哪里?它们可以存储在RAM中;这是默认设置。它们可以存储在计算机的硬盘驱动器或磁盘上,称为交换分区。有些字节可能存储在RAM中,有些字节可能存储在交换区中。常驻内存RAM快,而硬盘IO慢,但RAM很贵。通常计算机的硬盘驱动器空间比RAM多得多。例如,目前主流的电脑都有2T左右的硬盘存储空间,而内存却只有16GB。理想情况下,程序的所有内存都将存储在内存RAM中,但计算机上运行的各种进程可能会分配比RAM中可用的内存更多的内存。如果发生这种情况,操作系统会将一些数据从RAM移动或“交换”到硬盘驱动器。必要时,从交换分区中获取数据并交换未被主动使用的数据。现在我们准备好定义我们的第一个内存使用指标:常驻内存。常驻内存是进程分配的内存中有多少常驻或存储在RAM中。在第一个示例中,所有3GB的分配数组首先存储在RAM中。然后,当一些任务正在运行时,加载这些任务需要大量的RAM分配,因此操作系统会将一些数据从RAM交换到磁盘交换分区。结果,Python进程的常驻内存下降了:所有数据仍然可以访问,但其中一些被移动到磁盘交换分区。分配的内存可用于测量分配的内存,操作系统是将数据放在RAM中还是将其交换到磁盘,总是3GB内存,程序实际需要多少内存。在Python中(如果您使用的是Linux或macOS),您可以使用Fil内存分析器测量分配的内存,它专门测量峰值分配的内存。对于前面的例子:驻留内存和分配内存之间的权衡,驻留内存存在一些问题:内存使用和测量可能会受到其他进程的影响,因为其他进程可能会竞争驻留内存,导致实际使用的RAM会有所不同.常驻内存的上限是可用的物理RAM,因此一旦达到上限,您就永远不知道您的程序需要多少内存。例如主机的物理内存为16GB,那么需要17GB内存的程序和需要30GB内存的程序的常驻内存量是一样的,都是16GB。另一方面,分配的内存不受其他进程的影响,并告诉程序实际请求的是什么。当然,驻留内存确实比分配内存有优势:交换内存很可能永远不会被使用:想象创建一个数组,忘记删除引用,然后在程序的其余部分从未实际使用它。更广泛地说,由于驻留内存从操作系统的角度衡量实际使用的内存,因此它可以捕获分配内存跟踪不可见的边缘情况。让我们看一下这种边缘情况的示例。总结在目前的示例中,我们一直在分配充满1的数组。如果您测量分配的内存,则数组填充的内容没有区别:您可以切换到创建一个全为零的数组,但仍然会得到完全相同的结果。但是在Linux上,看另一个例子:importnumpyasnpiimportpsutilarr=np.zeros((1024,1024,1024,3),dtype=np.uint8)psutil.Process().memory_info().rss/(1024*1024)28.5546875这个此时,仍然分配了一个3GB的数组,但是赋予数组的元素全为零。然后测量常驻内存——数组还没有计算,常驻内存只有29M。数组占用的内存呢?事实证明,Linux不会费心将所有这些零存储在RAM中。相反,它只是在实际访问数据时将零块添加到RAM,而不实际分配内存。最后,应该提到的是,我们所说的内存使用模型也是很理想的。不包括文件缓存、分配器中的内存碎片或其他可用指标等。话虽如此,对于许多应用程序而言,分配的内存可能足以作为帮助优化程序内存使用的必要措施。