上接 单片机上跑Python慢?可以这样优化一下速度(一) ,本节接着介绍...
MicroPython代码优化
1> const()声明
MicroPython提供了const()声明的语法,其工作方式类似于C语言中的#define,当代码编译为字节码时,编译器会用数字常量值代替该标识符。这能够避免在代码运行时再进行字典查找,从而提升效率。const()的参数能够为任何在编译时等效为整数的数据,比如0x100或者1<<8等等。
2> 缓存对象引用
当函数或方法重复访问某对象时,可以通过将对象缓存在局部变量中来提高性能:
class foo(object):
def __init__(self):
self.ba = bytearray(100)
def bar(self, obj_display):
ba_ref = self.ba
fb = obj_display.framebuffer
# iterative code using these two objects
如上代码可以降低在bar()方法中重复查询self.ba和obj_display.framebuffer的必要性。
3> 控制垃圾回收
当需要内存分配时,MicroPython会尝试在堆中找到一个足够大的内存块。该过程可能会失败,通常是由于在堆内存中堆满了不再被代码引用的对象。如果确实失败了,垃圾回收进程会回收这些冗余对象所使用的内存,然后再次尝试进行分配——该过程可能要耗费几毫秒。
那么,通过定期调用gc.collect()则会带来一些好处。首先,在实际需要之前即进行收集要快得多——如果频繁进行该过程,其通常耗费1毫秒左右。其次,这样你便可以确定垃圾回收在代码中执行的时间点,而不是等到某个随机时间点再执行,而需要更长的延迟(可能在速度关键的代码部分)。最后,定期执行垃圾回收可以减少堆中的碎片,严重的碎片可能会导致不可恢复的内存分配失败。
本地代码生成器
这使得MicroPython编译器能够直接生成本地CPU操作码,而不是字节码。该特性可涵盖MicroPython的大部分函数,因此多数函数都不需要修改即可得到支持。其通过函数修饰器来使用:
@micropython.native
def foo(self, arg):
buf = self.linebuf # Cached object
# code
在当前版本的本地代码生成器中还存在着些许限制:
- 不支持上下文管理器(with语句);
- 不支持迭代生成器(Generator);
- 如果使用了raise语法,则必须提供参数。
本地码执行速度大约是字节码速度的两倍,而性能提高的代价是编译代码大小的增加。
Viper代码生成器
上面讨论的本地代码可与标准Python代码兼容,但接下来的Viper代码却不能与其完全兼容。为了追求性能,MicroPython支持特定的Viper本地数据类型,但整数类型是不兼容的,因为它使用的是机器字:32位硬件上的算术运算是以2**32为模进行的。
像本地生成器一样,Viper也生成机器指令,但其执行了进一步的优化,从而大大提高了性能,特别是整数运算和位操作。其通过如下函数修饰器来使用:
@micropython.viper
def foo(self, arg: int) -> int:
# 代码...
像上面代码段所示这样来使用python类型提示以帮助Viper优化器是有好处的。类型提示可以提供有关参数和返回值的数据类型信息,其在标准python语言特性里有定义,具体可参见PEP0484。Viper支持其自己的类型集合,比如int,uint(无符号整数),ptr,ptr8,ptr16和ptr32。关于ptrX类型,下面会再讨论。目前,uint类型只服务于一个目的:作为函数返回值的类型提示。如果这样的一个函数返回了0xffffffff,python会将该结果解释为2**32-1,而不是-1。
除了本地生成器的限制外,Viper生成器还有如下使用限制:
- 函数最多可以有四个参数;
- 不允许使用默认参数值;
- 也可以使用浮点数,但没有相应优化行为。
Viper提供了指针类型以协助优化器,具体包括:
- ptr 对象指针
- ptr8 字节指针
- ptr16 16位半字指针
- ptr32 32位机器字指针
指针的概念可能对于python程序员而言不太熟悉,其与python memoryview对象的相似之处在于:它提供了对存储在内存中数据的直接访问方法。数据项可以使用下标进行访问,但不支持切片:指针只能返回单个数据项。其目的是提供对存储在连续内存位置的数据的快速随机访问,比如存储在支持缓冲协议的对象中的数据,以及微控制器中的内存映射寄存器。值得注意的是,使用指针编程是非常危险的:其不执行边界检查,编译器也不采取任何措施来防止缓冲区溢出错误。
其典型用法是缓存变量:
@micropython.viper
def foo(self, arg: int) -> int:
buf = ptr8(self.linebuf) # self.linebuf 为bytearray或bytes对象
for x in range(20, 30):
bar = buf[x] # Access a data item through the pointer
# code omitted
在如上实例中,编译器"知道"buf是字节数组的地址,其可以生成相应代码以在运行时快速计算buf[x]的地址。当需要将普通对象转换为Viper本地类型时,应该在函数开始处,而不是在关键的定时循环处执行,因为这种转换可能需要耗费几微妙的时间。转换规则如下:
- 目前转换操作符有:int,bool,uint,ptr,ptr8,ptr16和ptr32;
- 转换的结果为本地Viper变量;
- 转换操作的参数可以为python对象或本地Viper变量;
- 如果参数已经是本地Viper变量,转换操作并无实质动作,其仅仅改变了类型(比如从uint转换为ptr8),以至于你能使用指针来存取数据;
- 如果参数为python对象而转换类型为int或uint,那么python对象必须为整数类型,该操作会返回其对应整数值;
- bool强制转换的参数必须是整数类型(布尔值或整数);当其用作返回类型时,Viper函数会返回True或False对象。
- 如果参数为python对象,强制转换为ptr,ptr8,ptr16或ptr32,则python对象要么必须有缓冲协议(该情况下返回指向缓冲区指针),要么必须是整数类型(该情况下返回该整数对象的值)。
写入指向只读对象的指针将导致未定义行为。
以下示例说明了如何使用ptr16转换来翻转X1引脚状态n次:
BIT0 = const(1)
@micropython.viper
def toggle_n(n: int):
odr = ptr16(stm.GPIOA + stm.GPIO_ODR)
for _ in range(n):
odr[0] ^= BIT0
直接硬件访问
注意:本节所用代码示例适用于Pyboard,但所述技巧也可用于其它的移植版本。
直接硬件访问属于更高级的编程范畴,其涉及到所用单片机的一些基础知识。考虑如下在Pyboard上切换输出引脚状态的示例代码。其标准写法为:
mypin.value(mypin.value() ^ 1) # mypin 为实例化的输出引脚
这涉及对Pin实例对象value()方法的两次调用开销。这种开销可以通过对芯片GPIO端口数据寄存器(odr)的对应位域执行读/写操作来消除。为了便于实现,stm模块提供了一组常数来对应相关寄存器的地址。快速翻转P4引脚(CPU引脚A14)——对应于绿色LED——能够采用如下方式进行:
import machine
import stm
BIT14 = const(1 << 14)
machine.mem16[stm.GPIOA + stm.GPIO_ODR] ^= BIT14
注, 本篇完结,欢迎关注哦...