micropython比c_CPython和MicroPython中的内存使用

Python部落(python.freelycode.com)组织翻译,禁止转载,欢迎转发。

在PyCon 2017上,Kavya Joshi关注了Python引用的实现在CPython和MicroPython上的不同。她特别描述了二者在内存使用和处理上的区别。这些不同是使MicroPython能运行在内存严重受限的微控制器上的原因之一,而CPython很难在这种环境上运行。

她在演讲中提到CPython是标准和默认的实现,大家都喜欢并一直在使用它。但在内存使用方面,CPython的声誉并不怎么样。这催生了一些替代方案的产生,MicroPython正是其中之一。

作为“最小最严苛”的替代方案,MicroPython以微控制器作为目标,从而使得Python可以用来控制硬件。MicroPython可以运行在16KB

RAM和256KB

ROM的设备上;它实现了Python3.4绝大部分的功能。由于诸如元类、多进程这样的内容在微控制器上没有多大意义,所以语言和标准库中的这一部分被删除了。

Joshi说道,CPython和MicroPython在后台颇为相似。它们都是基于堆栈的虚拟机的字节码解释器。它们都是C程序。然而,在内存使用方面,它们却截然相反。

作为一个简单的试验,没有任何形式的基准,她测试了Python3.6和MicroPython1.8在一台4GB运存的系统为64位Ubuntu

16.10的电脑上的表现。测试内容是创建20万个不同类型的对象(整数,字符型,列表)并保证它们不被回收。然后,她从Python本身中测量了程序的堆使用情况。

CPython和MicroPython都使用自定义分配器管理堆。但CPython根据需要增加堆,而MicroPython则使用一个固定大小的堆。由于Joshi测量的是堆的使用情况,这个固定大小也就是测量值的上限。但是,正如我们所见,MicroPython和CPython使用堆的方式并不一样,这也会影响到测量值。

从整数测试开始,她展示了堆使用情况的图表(Joshi的幻灯片在这里Speaker Deck)。该测试为从10的10次方(在Python中表示为10

**

10)开始的20万个连续整数创造了整型对象。CPython的图表显示堆使用量在线性增长,而MicroPython则完全是一条直线。字符对象的测试也是相同的结果。CPython的堆使用比MicroPython更多,但实际数字并不那么重要,真正让她感兴趣的是图形的形状。

但是,列表测试的结果则略有不同。该测试创建了一个列表,然后持续将很小的整型添加到列表中;结果图表显示堆使用和列表一样越来越大。CPython和MicroPython的结果图都显示了堆使用量的阶梯函数,但MicroPython每一步的高度和深度看上去都是CPython的两倍,而CPython每一步的增加则显得平缓一些。她想回答的问题之一是为什么这两个解释器在内存足迹和内存剖面上有如此大的不同。

对象和内存

为了解释到底发生了什么,她需要展示两个解释器内部是如何实现对象的。既然CPython在测试中使用了更多的内存,一定有内部的原因。不是CPython的对象要大一些就是CPython分配了超过它们大小的内存-也许二者兼有。

CPython把所有对象都分配在堆上,所以语句“x

=

1”将会为“1”在堆上创建一个对象。每个对象包含两个部分,头部(PyObject_HEAD)和一些可变对象的特定字段。头部是对象的开销,包括两个8字节字段,一个是引用计数器,一个是指向对象类型的指针。这意味着16字节是一个CPython对象大小的下限。

对一个整型对象来说,它的对象特定区域包括一个8字节长的标志记录后面有多少个四字节值(记住Python可以代表任意大小的整数)。所以存储一个值小于230-1的整数对象(有两个字节用于其他用途)需要28字节,其中有24个字节是额外的开销。而对测试中的值(1010以上),每个对象将占用32个字节,故20万个整数对象将要消耗6MB的内存。

而在MicroPython中,每个对象只有8字节。这八个字节既可以直接使用,也可以作为 指向其他数据结构的指针。指针标注允许八字节对象有多个用途。它被用来编码对象的一些额外信息;由于所有的内存地址都是八字节边界的别名,三个低位字节可以用来存储标签值。但并不是所有的标签位都是这样使用的;如果低序列位是一个,表明是存储在另外63位的小整数。一个102 在低序的两位代表剩余数之中的62位指向一个网络字符串(interned string)。002在这些位上表明其他位是指向具体对象的指针(即一些既不是小整型或者网络字符串的对象)。

由于测试中储存的值将会以小整数来对待,因此每个对象只占用8字节。这些都存储在栈上而不是堆,这也就解释了为什么MicroPython的测试结果是一条水平线。Joshi说道,即使你去测试栈的使用情况(可能栈的测量会有点复杂),MicroPython也要比CPython的内存使用少得多。

对于创建20万个长度小于10的字符串对象的测试来说,原理也是相似的。CPython中ASCII字符串对象(PyASCIIObject)使用48字节的头部,所以一个长度为1两个字节的字符串(如“a”加一个空白终止符)占用50个字节;一个长度为10的字符串将占用59个字节。MicroPython将小的字符串(长度小于254)存储为使用3字节头部的数组。所以长度为1的字符串占用5字节,只相当于CPython中需要空间的十分之一。

这仍然不足以解释结果上呈现的水平线。她说道,MicroPython中的字符串存储为一个预分配的数组。因此当字符串对象创建时并没有新堆进行分配。实际上,分配总是及时移除的,因此在图像上看不到堆的增加。

可变对象

字符串和证书都是不可变对象,但第三项测试使用了可变对象--列表。在CPython中,可变对象以PyGC_HEAD结构体增加了头部。这个结构用来进行垃圾回收跟踪,占用24个字节。总的来说,一个PyListObject长度为64字节;但你同样需要为列表中的每个对象添加指针,这些指针存储在一个数组中,并分配每个对象的存储空间。

列表测试结果图中的阶梯表明列表元素的数组动态调整大小的变化。如果每次追加操作都会进行大小的调整,结果图将会和整型、字符型的线性递增变化类似,但CPython提前为后期的追加操作分配了内存。这样的机制稀释了多重拼接操作的内存分配的开销。

MicroPython的垃圾回收机制和CPython完全不同,它并没有使用引用计数。所以,MicroPython中用于列表的具体的对象并没有引用计数;但它也和CPython一样使用了类型指针。和CPython相比,移除引用计数器节约了八字节的内存。另外,MicroPython也没有为垃圾回收使用额外的头部,节约了24字节。这样,与CPython相比,MicroPython中的可变对象共节约了32字节。

所以结果就是CPython对象要比MicroPython大很多。其他对象类型,尤其是类,在和CPython3.6比起来就没有这么明显的差距了,Joshi补充道,因为新版本的CPython进行了优化,大大减少了这方面的开销。除此之外,CPython在测试中比MicroPython分配了更多的对象。MicroPython直接把整型对象存储在栈上,而不像CPython那样将其分配在堆上。

垃圾回收

头部如何跟踪CPython对象的垃圾回收可能是观众们最不容易理解的一部分,Joshi说。CPython使用引用计数器,可以很容易得追溯一个对象的引用;每当一个对象被复制,或者放入列表,插入一个字典等等,它的计数器都会增加。而当一个引用消失,例如使用del()操作或者一个变量超出范围,计数器相应得减少。而当计数器为0时,对象将会被回收。

但也并不是所有情况都是这样的,这是因为PyGC_HEAD结构体在起作用。如果一个对象引用自它们本身或者间接包含对自身的引用,会构成循环引用。她举了个例子:

如果x随后超出范围或者执行了del(x)操作,引用计数器并不会变为0,而对象也永远不会被回收。CPython使用一个循环垃圾回收器来检测中断循环引用,解决这种问题。由于只有可变对象可能会有这样的循环引用,它们是唯一使用PyGC_HEAD信息来进行跟踪的对象。

MicroPython使用位图追踪堆的分配。它将堆分解成32字节的分配单元,每个单元使用两位来记录回收或者使用中的追踪信息。通过使用一个记录、扫描垃圾回收器在定期运行中维护位图;它管理了所有分配在堆上的对象。通过这种方式,标记扫描方法有效得以执行时间换取了跟踪内存信息开销的减少。

Joshi说她仅仅用了很肤浅的优化就减少了MicroPython的内存使用。但代码是可用的; 它很容易理解,感兴趣的话大家都可以看看。

不同的方法

在分享的结尾,她总结了为什么CPython和MicroPython选择各自的方法。一般情况下,我们总要在内存使用和性能之间做出权衡,但本文的例子并非如此。MicroPython在大部分的性能测试中都优于CPython,

Joshi说,尽管它确实可能在一些性能测试中表现的更差尤其是一些包含巨大字典的场景中。这里的权衡是功能性上的,MicroPython并没有实现CPython的完整特性。这意味着,例如MicroPython的使用者无法像CPython的开发者那样能享受第三方库的完整生态。

CPython和MicroPython项目做出的不同设计决策有一部分原因是其发展背后的哲学。回到CPython刚被发明的20世纪八十年代末九十年代初,开发者当时想要的是“简单的实现和设计优先”,他们将会在后面考虑性能的问题。

这和MicroPython背后的哲学恰恰相反。它是一个比较新的实现,所以可以从CPython产生后的十多年发展出来的诸如Lua、JavaScript或其他语言借鉴一些新想法。MicroPython也仅仅针对这些高度受限的微控制器来服务。事实上,这些微控制器的目标是很多不同的系统和使用场景,这也可以帮助解释其做选择的原因。

英文原文:https://lwn.net/Articles/725508/

译者:mrwoody

Tag标签:

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值