在前一篇文章中,已经介绍了单片机上进行Python编程的一些注意点,详见:单片机上运行Python——MicroPython(一)。本篇就接着介绍后续内容,想要获取更多内容可以关注我哦。
堆空间
当运行中的程序实例化对象时,所需的RAM资源会从一个固定大小的内存池中被申请和分配,该内存池即为堆空间。当对象超过作用域后(或者说其不再可用时),该对象即被称作"垃圾"资源。此时,一个单独的进程,被称作"垃圾回收器",会收回其所占内存并将其内存返回到可用的堆空间中。该进程会在后台自动运行,你也可以直接显式地使用gc.collect()对该过程进行调用。
如果感觉上述论述稍微有一点点复杂,只需要记住:周期性地进行如下调用即可。
gc.collect()
gc.threshold(gc.mem_free() // 4 + gc.mem_alloc())
内存碎片
比如说一个程序,创建了对象foo后又创建了对象bar。接着,foo超出了作用域但bar仍有效。foo所使用的RAM资源将会被垃圾回收器收回。然而,如果bar分配在高地址空间,仅当新对象空间不大于foo当初所申请的内存大小时,其所收回的空间才可被再次使用。在一个复杂且长期运行的程序中,堆栈因此会变得碎片化:尽管仍然有大量RAM空间可用,但没有充足的连续的空间可用于新对象实例化使用。此时程序便会陷入到内存错误之中。
上面章节所述的编程技巧均旨在减少内存碎片的产生。如果需要使用大的永久性缓冲或其它类型对象,最好在程序运行的早期就进行实例化,以避免内存碎片的影响。若要更进一步改善这种情况,可以监视堆栈的状态并手动控制垃圾回收器的执行。大致方法会在下面章节介绍。
内存状态报告
有许多库函数可用于报告内存的分配情况和控制垃圾回收器的执行。其多存在于gc模块和micropython模块。可将下面的示例代码粘贴到REPL中运行查看效果。(Ctrl+E 进入粘贴模式,然后Ctrl+D运行)
import gc
import micropython
gc.collect()
micropython.mem_info()
print('-----------------------------')
print('Initial free: {} allocated: {}'.format(gc.mem_free(), gc.mem_alloc()))
def func():
a = bytearray(10000)
gc.collect()
print('Func definition: {} allocated: {}'.format(gc.mem_free(), gc.mem_alloc()))
func()
print('Func run free: {} allocated: {}'.format(gc.mem_free(), gc.mem_alloc()))
gc.collect()
print('Garbage collect free: {} allocated: {}'.format(gc.mem_free(), gc.mem_alloc()))
print('-----------------------------')
micropython.mem_info(1)
上面使用的函数如下:
- gc.collect() 强制垃圾回收
- micropython.mem_info() 打印RAM使用情况汇总
- gc.mem_free() 返回堆栈剩余内存空间字节数
- gc.mem_alloc() 返回当前已分配的内存字节数
- micropython.mem_info(1) 以表格形式打印出RAM使用情况
运行时具体产生的结果数据和所用平台有关,但应该能够看到,函数的声明使用了一小部分RAM空间,这部分空间由编译器回收所得。函数运行使用了超过10KB的空间,但其返回时a变成了垃圾资源,因为其已经超出了作用域,不能再被引用了。最后的gc.collect()恢复了内存。
micropython.mem_info(1) 最终的输出细节依平台不同而不同,但其中符号可大致按如下解释:
每个字母代表一个单独的内存块,大小为16字节。所以,堆栈打印输出的每一行代表0x400字节或者1KB的RAM空间。
控制垃圾回收器
任何时候都可以通过调用gc.collect()进行垃圾回收。周期性地进行该操作对系统运行十分有利:既可以减少内存碎片又能够提高系统性能。垃圾回收过程会占用毫秒级的时间,当其它工作较少时该过程会更快一点(在Pyboard上大约为1ms)。在程序的特定位置显式调用垃圾回收过程会最小化其时间延迟消耗。
垃圾回收会在下面情况下被自动触发。某次内存分配失败时,垃圾回收过程会自动执行,然后再重试之前内存分配动作,只有再次分配失败时才会抛出异常。如果可用RAM空间减少到一个阈值时,垃圾回收也会被自动触发。该阈值也能够随着程序的执行自动调整。
gc.collect()
gc.threshold(gc.mem_free() // 4 + gc.mem_alloc())
上述代码表示,当前可用堆栈的大于25%的空间被占用时会触发垃圾回收过程。
通常来讲,模块代码应该在运行时再通过构造函数或其它初始化函数实例化数据对象。因为如果在模块被导入时便实例化数据对象,可能会导致编译器内存不足而无法继续导入后续的模块。如果模块确实需要在被导入时便实例化数据,之后随即调用gc.collect()会改善其所带来的问题。
字符串操作
MicroPython会高效地处理字符串,理解这一点有助于设计能够在单片机上良好运行的程序。当模块被编译时,其中多次出现的字符串会仅存储一次,该过程被称作字符串内嵌化。在MicroPython中,内嵌的字符串被称作qstr。通常,被导入的模块实例存储于RAM空间中,但如以上所述,固化为字节码的模块则会存储于Flash上。
字符串比较也比较高效,其内部使用哈希比较而不是字符对字符的方式。使用字符串而不是整数所带来的代价因此会特别小,无论是从性能方面考虑还是从内存占用方面考虑——这也许对于C程序员而讲比较惊讶。
附记
(1) MicroPython传递、返回和复制(默认情况下)对象时均以引用的方式进行。引用变量仅占用单字存储空间,所以该过程在RAM占用和速度方面效率很高。
当要求使用完整的变量时,不能以字节存储空间或字存储空间引用形式使用,MicroPython中有标准的库,有助于高效地存储这些变量并进行相关类型转换。可以查阅array,ustruct和uctypes模块获取详情。
(2) 在Unix和Windows平台上,gc.collect()函数返回一个整数,其表明本次回收的不同的内存区块数(更准确来说,变得可用的内存块的数目)。考虑到效率方面的原因,单片机上运行的MicroPython版本没有返回值。