返回介绍

11.1 基础类型的对象开销高

发布于 2024-01-25 21:44:08 字数 5379 浏览 0 评论 0 收藏 0

使用存储着几百上千项的类似于list的容器来工作是普遍的。一旦你存储大数据,RAM的使用就变成了一个问题。

一个具有100000000项的list大概要消耗760MB,这是在假设所有条目都是相同对象的前提下。如果我们存储了100000000个不同项(例如,唯一的整数),那么我们得期望使用GB数量级的RAM!每一个唯一的对象都有一个内存开销。

在例11-1中,我们在一个list中存储了许多0整数。如果你存储了100000000个任意对象的引用(无论对象的实例有多大),你还是期望要看见大约760MB的内存开销,因为list存储着对象的引用(不是对象的拷贝)。回头参考下2.9节来回忆怎样使用memory_profile;在这里,我们使用%load_ext memory_profile来把它作为一个新的魔法函数载入进IPython。

例11-1 测量在一个list中的100000000个相同整数的内存使用

In [1]: %load_ext memory_profiler # load the %memit magic function
In [2]: %memit [0]*int(1e8)
peak memory: 790.64 MiB, increment: 762.91 MiB

对于下个例子,我们将从一个全新的shell开始。就如在例11-2中对memit的首次调用所揭示的那样,一个全新的IPython shell大约消耗了20MB的RAM。接下来,我们可以创建一个具有100000000个唯一数字的临时list。这总共大约消耗3.1GB。

 警告 

在运行的进程中,内存能够被缓存起来,所以当使用memit来剖析时,先退出再重启Python shell总是更安全的方式。

在memit命令结束后,临时list被释放了。最终对memit的调用显示内存使用停留在大约2.3GB。

 问题 

在读取答案之前,为什么Python进程还是可能要保持2.3GB的RAM?在后台留下什么东西了吗,即使list已经到垃圾收集器中去了?

例11-2 测量一个list中100000000个不同整数的内存使用

# we use a new IPython shell so we have a clean memory
In [1]: %load_ext memory_profiler
In [2]: %memit # show how much RAM this process is consuming right now
peak memory: 20.05 MiB, increment: 0.03 MiB
In [3]: %memit [n for n in xrange(int(1e8))]
peak memory: 3127.53 MiB, increment: 3106.96 MiB
In [4]: %memit
peak memory: 2364.81 MiB, increment: 0.00 MiB

100000000个整数对象占据了2.3GB的绝大部分,即使它们不再被使用了。Python缓存了类似整数的基础对象为以后所用。在一个RAM有限的系统中,这会造成问题,所以你应该注意到这些基础类型可能会构建在缓存中。

在例11-3中一个后续的memit创建了另一个含有100000000项的list,消耗了大约760MB,在这个回调期间总体占用了大约达到3.1GB的内存分配。760MB单单为容器所用,因为底层的Python整数对象已经存在——它们在缓存中,这样就可以被复用。

例11-3 再次测量在一个list中的100000000个不同整数的内存使用

In [5]: %memit [n for n in xrange(int(1e8))]
peak memory: 3127.52 MiB, increment: 762.71 MiB

接下来我们将看到我们能够使用array模块来以更为廉价的方式存储100000000个整数。

Array模块以廉价的方式存储了许多基础对象

Array模块高效地存储了类似于整数、浮点数和字符的基础类型,但没有复数或者类。它创建了一个连续的RAM块来保存底层数据。

在例11-4中,我们把100000000个整数(每个8字节)分配到一个连续的内存块。总体上,进程大约消耗了 760MB。这种方式和之前唯一整数列表的方式之间的差别是2300MB – 760MB == 1.5GB。这是一个对RAM的巨大节约。

例11-4 构建一个使用760MB的RAM的具有100000000个整数的数组

In [1]: %load_ext memory_profiler
In [2]: import array
In [3]: %memit array.array('l', xrange(int(1e8)))
peak memory: 781.03 MiB, increment: 760.98 MiB
In [4]: arr = array.array('l')
In [5]: arr.itemsize
Out[5]: 8

注意在array中的唯一数字不是Python对象,它们在array中是字节。如果我们要解引用它们中任何一个,那么一个新的Python int对象将会被构建。如果你想要在它们之上来做计算,不会发生整体上的节省,但是如果你想要把数组传递给一个外部进程或者只使用一些数据,你应该看到相比使用一个整数的list来说,大大节约了RAM。

 备忘 

如果你正使用Cython在一个大数字数组或大数字矩阵上工作,并且你不想要对numpy的外部依赖,提醒你可以把你的数据存储在一个array中,并把它传进Cython来做处理,这样没有额外的内存开销。

array模块使用一个有限的具有各种不同精度的datatype集(请看例11-5)来工作。选择你需要的最小精度,这样你就会仅仅按需分配RAM,而不是分配超出需求更多的RAM。要注意字节的大小是平台相关的——这里的大小参考32位的平台(它声明了最小尺寸),而我们却是在一台64位的笔记本电脑上运行例子的。

例11-5 由array模块所提供的基本类型

In [5]: array? # IPython magic, similar to help(array)
Type:     module
String Form:<module 'array' (built-in)>
Docstring:
This module defines an object type which can efficiently represent
an array of basic values: characters, integers, floating point
numbers. Arrays are sequence types and behave very much like lists,
except that the type of objects stored in them is constrained. The
type is specified at object creation time by using a type code, which
is a single character. The following type codes are defined:

  Type code   C Type       Minimum size in bytes
  'c'     character      1
  'b'     signed integer   1
  'B'     unsigned integer   1
  'u'     Unicode character  2
  'h'     signed integer   2
  'H'     unsigned integer   2
  'i'     signed integer   2
  'I'     unsigned integer   2
  'l'     signed integer   4
  'L'     unsigned integer   4
  'f'     floating point   4
  'd'     floating point   8

The constructor is:

array(typecode [, initializer]) -- create a new array

numpy具有能够持有更广泛的datatypes的数组——你对每一项的字节的数量有更多的控制,并且你还可以使用复数和datetime对象。一个complex128对象采用每项16个字节:每项是一个8字节的浮点数对。你不能在一个Python数组中存储复杂的对象,但是它们在numpy中是自由使用的。如果你是一个numpy的新手,请回去看看第6章。

在例11-6中,你能看见numpy数组的另一个特性——你可以查询项的数量、每个基础类型的大小以及底层RAM块的组合存储总量。注意这不包括Python对象的开销(一般情况下,相比你存储在数组中的数据而言,这是微不足道的)。

例11-6 在numpy数组中存储更多的复杂类型

In [1]: %load_ext memory_profiler
In [2]: import numpy as np
In [3]: %memit arr=np.zeros(1e8, np.complex128)
peak memory: 1552.48 MiB, increment: 1525.75 MiB
In [4]: arr.size # same as len(arr)
Out[4]: 100000000
In [5]: arr.nbytes
Out[5]: 1600000000
In [6]: arr.nbytes/arr.size # bytes per item
Out[6]: 16
In [7]: arr.itemsize # another way of checking
Out[7]: 16

使用一个常规的list在RAM中来存储许多数字比使用一个array对象要低效得多。应当发生更多的内存分配,每一次都花费时间。在更大的对象上也发生了运算,对缓存更不友好,整体上使用了更多的RAM,这样一来可用于其他程序的RAM就更少了。

无论如何,如果你在Python的array内容上做任何工作,基础类型可能被转换成临时对象,抵消了它们的收益。当和其他进程通信时把它们当成数据存储来使用是array的一个很棒的使用场景。

如果你正在做重量级的数字运算,那么numpy数组几乎肯定是一个更好的选择,因为你得到了更多的datatype选项和许多专业而快速的函数。如果你想要让你的项目有更少的依赖性,你可能选择避开numpy,尽管Cython和Pythran用array和numpy数组同样都工作得好。Numba只用numpy数组来工作。

Python提供了一些其他工具来理解内存使用,就如我们将要在下一节中看到的那样。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文