การเพิ่มความเร็วสูงสุดใน MicroPython

บทแนะนำนี้อธิบายวิธีการปรับปรุงประสิทธิภาพของโค้ด MicroPython การเพิ่มประสิทธิภาพที่เกี่ยวข้องกับภาษาอื่นจะครอบคลุมในหัวข้ออื่น ได้แก่ การใช้โมดูลที่เขียนด้วยภาษา C และ MicroPython inline assembler

กระบวนการพัฒนาโค้ดประสิทธิภาพสูงประกอบด้วยขั้นตอนต่อไปนี้ซึ่งควรดำเนินการตามลำดับที่ระบุไว้

  • ออกแบบเพื่อความเร็ว

  • เขียนโค้ดและแก้ไขข้อบกพร่อง

ขั้นตอนการเพิ่มประสิทธิภาพ:

  • ระบุส่วนของโค้ดที่ช้าที่สุด

  • ปรับปรุงประสิทธิภาพของโค้ด Python

  • ใช้ native code emitter

  • ใช้ viper code emitter

  • ใช้การเพิ่มประสิทธิภาพเฉพาะฮาร์ดแวร์

การออกแบบเพื่อความเร็ว

ปัญหาด้านประสิทธิภาพควรได้รับการพิจารณาตั้งแต่เริ่มต้น ซึ่งเกี่ยวข้องกับการมองภาพรวมของส่วนโค้ดที่มีความสำคัญด้านประสิทธิภาพมากที่สุด และให้ความสนใจเป็นพิเศษกับการออกแบบส่วนเหล่านั้น กระบวนการเพิ่มประสิทธิภาพจะเริ่มต้นหลังจากทดสอบโค้ดแล้ว: หากการออกแบบถูกต้องตั้งแต่แรก การเพิ่มประสิทธิภาพจะทำได้ง่ายและอาจไม่จำเป็นต้องทำเลย

อัลกอริทึม

ด้านที่สำคัญที่สุดในการออกแบบรูทีนใดๆ เพื่อประสิทธิภาพคือการตรวจสอบให้แน่ใจว่าใช้อัลกอริทึมที่ดีที่สุด หัวข้อนี้เหมาะสำหรับตำราเรียนมากกว่าคู่มือ MicroPython แต่บางครั้งก็สามารถบรรลุผลการปรับปรุงประสิทธิภาพที่น่าประทับใจได้โดยการนำอัลกอริทึมที่มีชื่อเสียงด้านประสิทธิภาพมาใช้

การจัดสรร RAM

เพื่อออกแบบโค้ด MicroPython ที่มีประสิทธิภาพ จำเป็นต้องเข้าใจวิธีที่ตัวแปลภาษาจัดสรร RAM เมื่อสร้างออบเจกต์หรือขนาดเพิ่มขึ้น (เช่น เมื่อเพิ่มรายการลงในลิสต์) RAM ที่จำเป็นจะถูกจัดสรรจากบล็อกที่เรียกว่า heap ซึ่งใช้เวลาค่อนข้างมาก นอกจากนี้บางครั้งจะกระตุ้นกระบวนการที่เรียกว่า garbage collection ซึ่งอาจใช้เวลาหลายมิลลิวินาที

ดังนั้นประสิทธิภาพของฟังก์ชันหรือเมธอดสามารถปรับปรุงได้หากสร้างออบเจกต์เพียงครั้งเดียวและไม่อนุญาตให้ขนาดเพิ่มขึ้น ซึ่งหมายความว่าออบเจกต์จะคงอยู่ตลอดระยะเวลาการใช้งาน: โดยทั่วไปจะถูกสร้างขึ้นใน class constructor และใช้ในเมธอดต่างๆ

หัวข้อนี้ได้รับการครอบคลุมในรายละเอียดเพิ่มเติมใน Controlling garbage collection ด้านล่าง

บัฟเฟอร์

ตัวอย่างของข้างต้นคือกรณีทั่วไปที่ต้องใช้บัฟเฟอร์ เช่น บัฟเฟอร์ที่ใช้สำหรับการสื่อสารกับอุปกรณ์ ไดรเวอร์ทั่วไปจะสร้างบัฟเฟอร์ใน constructor และใช้ในเมธอด I/O ซึ่งจะถูกเรียกซ้ำๆ

ไลบรารี MicroPython โดยทั่วไปจะรองรับบัฟเฟอร์ที่จัดสรรล่วงหน้า ตัวอย่างเช่น ออบเจกต์ที่รองรับอินเทอร์เฟซสตรีม (เช่น ไฟล์หรือ UART) จะมีเมธอด read() ซึ่งจัดสรรบัฟเฟอร์ใหม่สำหรับข้อมูลที่อ่าน แต่ยังมีเมธอด readinto() เพื่ออ่านข้อมูลลงในบัฟเฟอร์ที่มีอยู่แล้ว

คลาสที่มีประโยชน์บางประการสำหรับการสร้างออบเจกต์บัฟเฟอร์ที่นำกลับมาใช้ใหม่ได้:

จุดทศนิยม

MicroPython บางพอร์ตจัดสรรตัวเลขทศนิยมบน heap บางพอร์ตอื่นอาจไม่มี coprocessor ทศนิยมเฉพาะ และประมวลผลการดำเนินการทางคณิตศาสตร์บนตัวเลขเหล่านั้นด้วย "ซอฟต์แวร์" ซึ่งช้ากว่าจำนวนเต็มอย่างมาก เมื่อประสิทธิภาพสำคัญ ให้ใช้การดำเนินการจำนวนเต็มและจำกัดการใช้ทศนิยมไว้ในส่วนของโค้ดที่ประสิทธิภาพไม่ใช่สิ่งสำคัญที่สุด เช่น บันทึกค่าที่อ่านจาก ADC เป็นค่าจำนวนเต็มลงในอาร์เรย์ในการดำเนินการเดียว จากนั้นจึงแปลงเป็นตัวเลขทศนิยมสำหรับการประมวลผลสัญญาณ

อาร์เรย์

พิจารณาการใช้ประเภทคลาสอาร์เรย์ต่างๆ เป็นทางเลือกแทนลิสต์ โมดูล array รองรับประเภทองค์ประกอบต่างๆ โดยมีองค์ประกอบ 8 บิตที่รองรับโดยคลาส bytes และ bytearray ที่ built-in ของ Python โครงสร้างข้อมูลเหล่านี้ทั้งหมดจัดเก็บองค์ประกอบในตำแหน่งหน่วยความจำที่ต่อเนื่องกัน อีกครั้ง เพื่อหลีกเลี่ยงการจัดสรรหน่วยความจำในโค้ดที่สำคัญ ควรจัดสรรล่วงหน้าและส่งผ่านเป็นอาร์กิวเมนต์หรือออบเจกต์ที่ผูกไว้

Memoryview

เมื่อส่ง slice ของออบเจกต์เช่นอินสแตนซ์ bytearray Python จะสร้างสำเนาซึ่งเกี่ยวข้องกับการจัดสรรตามสัดส่วนของขนาด slice ปัญหานี้สามารถบรรเทาได้โดยใช้ออบเจกต์ memoryview ตัว memoryview เองถูกจัดสรรบน heap แต่เป็นออบเจกต์ขนาดเล็กคงที่ โดยไม่คำนึงถึงขนาดของ slice ที่ชี้ไป การ slice memoryview จะสร้าง memoryview ใหม่ ดังนั้นจึงไม่สามารถทำได้ใน interrupt service routine นอกจากนี้ syntax ของ slice a:b จะทำให้เกิดการจัดสรรเพิ่มเติมโดยสร้างออบเจกต์ slice(a, b)

ba = bytearray(10000)  # big array
func(ba[30:2000])      # a copy is passed, ~2K new allocation
mv = memoryview(ba)    # small object is allocated
func(mv[30:2000])      # a pointer to memory is passed

memoryview สามารถใช้กับออบเจกต์ที่รองรับ buffer protocol เท่านั้น ซึ่งรวมถึงอาร์เรย์แต่ไม่รวมลิสต์ ข้อควรระวังเล็กน้อยคือในขณะที่ออบเจกต์ memoryview ยังทำงานอยู่ มันก็ยังคงออบเจกต์บัฟเฟอร์ต้นฉบับไว้ด้วย ดังนั้น memoryview ไม่ใช่ทางแก้ปัญหาที่ดีที่สุดในทุกกรณี ตัวอย่างเช่น หากคุณใช้งานบัฟเฟอร์ขนาด 10K เสร็จแล้วและต้องการเฉพาะไบต์ 30:2000 จากมัน อาจดีกว่าที่จะทำ slice และปล่อยให้บัฟเฟอร์ 10K ถูก garbage collect แทนที่จะสร้าง memoryview ที่มีอายุยาวและเก็บ 10K ไว้ขัดขวาง GC

อย่างไรก็ตาม memoryview ไม่สามารถขาดได้สำหรับการจัดการบัฟเฟอร์ที่จัดสรรล่วงหน้าขั้นสูง เมธอด readinto() ที่กล่าวถึงข้างต้นจะวางข้อมูลที่จุดเริ่มต้นของบัฟเฟอร์และเติมบัฟเฟอร์ทั้งหมด แล้วถ้าคุณต้องการวางข้อมูลตรงกลางของบัฟเฟอร์ที่มีอยู่ล่ะ? เพียงสร้าง memoryview เข้าไปในส่วนที่ต้องการของบัฟเฟอร์และส่งไปยัง readinto()

สตริงและไบต์

MicroPython ใช้ string interning เพื่อประหยัดพื้นที่เมื่อมีสตริงเหมือนกันหลายตัว ทุกครั้งที่สร้างสตริงใหม่ขณะรันไทม์ (เช่น เมื่อนำสตริงสองตัวมาต่อกัน) MicroPython จะตรวจสอบว่าสตริงใหม่นั้นสามารถ intern เพื่อประหยัด RAM ได้หรือไม่

หากคุณมีโค้ดที่ดำเนินการสตริงที่สำคัญต่อประสิทธิภาพ ให้พิจารณาใช้ออบเจกต์และค่าตัวอักษร bytes (เช่น b"abc"). ซึ่งข้ามการตรวจสอบ interning และอาจเร็วกว่าหลายเท่าเมื่อเทียบกับการดำเนินการเดียวกันด้วยออบเจกต์สตริง

Note

ประสิทธิภาพสูงสุดจะทำได้เสมอโดยการหลีกเลี่ยงการสร้างออบเจกต์ใหม่ทั้งหมด เช่น การใช้ buffer as described above ที่นำกลับมาใช้ใหม่ได้

การระบุส่วนของโค้ดที่ช้าที่สุด

กระบวนการนี้เรียกว่า profiling และครอบคลุมในตำราเรียน และ (สำหรับ Python มาตรฐาน) รองรับโดยเครื่องมือซอฟต์แวร์ต่างๆ สำหรับแอปพลิเคชัน embedded ขนาดเล็กที่มักจะทำงานบนแพลตฟอร์ม MicroPython ฟังก์ชันหรือเมธอดที่ช้าที่สุดมักสามารถระบุได้โดยการใช้กลุ่มฟังก์ชัน ticks ด้านการจับเวลาที่มีเอกสารอยู่ใน time อย่างชาญฉลาด เวลาในการประมวลผลโค้ดสามารถวัดได้เป็น ms, us หรือ CPU cycles

ต่อไปนี้ทำให้ฟังก์ชันหรือเมธอดใดๆ สามารถจับเวลาได้โดยการเพิ่ม decorator @timed_function:

def timed_function(f, *args, **kwargs):
    myname = str(f).split(' ')[1]
    def new_func(*args, **kwargs):
        t = time.ticks_us()
        result = f(*args, **kwargs)
        delta = time.ticks_diff(time.ticks_us(), t)
        print('Function {} Time = {:6.3f}ms'.format(myname, delta/1000))
        return result
    return new_func

การปรับปรุงโค้ด MicroPython

การประกาศ const()

MicroPython มีการประกาศ const() ซึ่งทำงานในลักษณะเดียวกับ #define ใน C ตรงที่เมื่อโค้ดถูกคอมไพล์เป็น bytecode คอมไพเลอร์จะแทนที่ค่าตัวเลขสำหรับตัวระบุ ซึ่งช่วยหลีกเลี่ยงการค้นหา dictionary ขณะรันไทม์ อาร์กิวเมนต์ของ const() อาจเป็นอะไรก็ได้ที่เวลาคอมไพล์ประเมินค่าเป็นจำนวนเต็ม เช่น 0x100 หรือ 1 << 8

การแคชการอ้างอิงออบเจกต์

เมื่อฟังก์ชันหรือเมธอดเข้าถึงออบเจกต์ซ้ำๆ ประสิทธิภาพจะดีขึ้นโดยการแคชออบเจกต์ในตัวแปรโลคัล:

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

ซึ่งช่วยหลีกเลี่ยงความจำเป็นในการค้นหา self.ba และ obj_display.framebuffer ซ้ำๆ ในส่วนของเมธอด bar()

การควบคุม garbage collection

เมื่อต้องการจัดสรรหน่วยความจำ MicroPython จะพยายามหาบล็อกที่มีขนาดเพียงพอบน heap ซึ่งอาจล้มเหลว โดยทั่วไปเนื่องจาก heap เต็มไปด้วยออบเจกต์ที่โค้ดไม่ได้อ้างอิงอีกต่อไป หากเกิดความล้มเหลว กระบวนการที่เรียกว่า garbage collection จะเรียกคืนหน่วยความจำที่ใช้โดยออบเจกต์ที่ซ้ำซ้อนเหล่านี้ จากนั้นจึงลองจัดสรรอีกครั้ง ซึ่งเป็นกระบวนการที่อาจใช้เวลาหลายมิลลิวินาที

อาจมีประโยชน์ในการป้องกันล่วงหน้าโดยการเรียก gc.collect() เป็นระยะ ประการแรก การทำ collection ก่อนที่จะจำเป็นจริงๆ จะเร็วกว่า โดยทั่วไปอยู่ในระดับ 1ms หากทำบ่อยๆ ประการที่สอง คุณสามารถกำหนดจุดในโค้ดที่ใช้เวลานี้แทนที่จะให้เกิดความล่าช้าที่นานขึ้นแบบสุ่ม ซึ่งอาจอยู่ในส่วนที่สำคัญต่อความเร็ว สุดท้าย การทำ collection เป็นประจำสามารถลดการแตกกระจายใน heap ได้ การแตกกระจายอย่างรุนแรงอาจนำไปสู่ความล้มเหลวในการจัดสรรที่ไม่สามารถกู้คืนได้

Native code emitter

ซึ่งทำให้คอมไพเลอร์ MicroPython ส่งออก CPU opcodes แบบ native แทน bytecode ครอบคลุมฟังก์ชันการทำงาน MicroPython ส่วนใหญ่ ดังนั้นฟังก์ชันส่วนใหญ่จะไม่ต้องปรับเปลี่ยน (แต่ดูด้านล่าง) มันถูกเรียกใช้โดย function decorator:

@micropython.native
def foo(self, arg):
    buf = self.linebuf # Cached object
    # code

มีข้อจำกัดบางประการในการใช้งาน native code emitter ปัจจุบัน

  • หากใช้ raise จะต้องระบุอาร์กิวเมนต์

  • background scheduler (ดู micropython.schedule) ไม่ทำงานระหว่างการประมวลผล native code

  • บนเป้าหมายที่มี threading และ GIL จะไม่ปล่อย GIL ระหว่างการประมวลผล native code

เพื่อบรรเทาสองประเด็นสุดท้าย ฟังก์ชัน native ที่ทำงานนาน ควรเรียก time.sleep(0) เป็นระยะ ซึ่งจะรัน scheduler และส่ง GIL กลับไปมา

การแลกเปลี่ยนสำหรับประสิทธิภาพที่ดีขึ้น (เร็วกว่า bytecode ประมาณสองเท่า) คือขนาดโค้ดที่คอมไพล์แล้วเพิ่มขึ้น

Viper code emitter

การเพิ่มประสิทธิภาพที่กล่าวถึงข้างต้นเกี่ยวข้องกับโค้ด Python ที่เป็นไปตามมาตรฐาน Viper code emitter ไม่ได้เป็นไปตามมาตรฐานอย่างสมบูรณ์ มันรองรับประเภทข้อมูล native ของ Viper เพื่อมุ่งไปสู่ประสิทธิภาพ การประมวลผลจำนวนเต็มไม่เป็นไปตามมาตรฐานเนื่องจากใช้ machine words: เลขคณิตบนฮาร์ดแวร์ 32 บิตดำเนินการ modulo 2**32

เช่นเดียวกับ Native emitter Viper จะสร้าง machine instructions แต่มีการเพิ่มประสิทธิภาพเพิ่มเติมซึ่งเพิ่มประสิทธิภาพอย่างมาก โดยเฉพาะสำหรับเลขคณิตจำนวนเต็มและการดำเนินการบิต มันถูกเรียกใช้โดย decorator:

@micropython.viper
def foo(self, arg: int) -> int:
    # code

ดังที่ fragment ข้างต้นแสดงให้เห็น การใช้ type hints ของ Python เพื่อช่วย Viper optimiser จะเป็นประโยชน์ Type hints ให้ข้อมูลเกี่ยวกับประเภทข้อมูลของอาร์กิวเมนต์และค่าที่ส่งกลับ ซึ่งเป็นฟีเจอร์ภาษา Python มาตรฐานที่กำหนดอย่างเป็นทางการที่นี่ PEP0484 Viper รองรับชุดประเภทของตัวเอง ได้แก่ int, uint (unsigned integer), ptr, ptr8, ptr16 และ ptr32 ประเภท ptrX จะกล่าวถึงด้านล่าง ปัจจุบันประเภท uint มีจุดประสงค์เดียว: เป็น type hint สำหรับค่าที่ส่งกลับของฟังก์ชัน หากฟังก์ชันดังกล่าวส่งคืน 0xffffffff Python จะตีความผลลัพธ์ว่าเป็น 2**32 -1 แทนที่จะเป็น -1

นอกเหนือจากข้อจำกัดที่กำหนดโดย native emitter ยังมีข้อจำกัดต่อไปนี้ที่บังคับใช้:

  • ไม่อนุญาตให้ใช้ค่าอาร์กิวเมนต์เริ่มต้น

  • อาจใช้ floating point ได้แต่ไม่ได้รับการเพิ่มประสิทธิภาพ

Viper มีประเภทพอยน์เตอร์เพื่อช่วย optimiser ซึ่งประกอบด้วย

  • ptr พอยน์เตอร์ไปยังออบเจกต์

  • ptr8 ชี้ไปยังไบต์

  • ptr16 ชี้ไปยัง half-word ขนาด 16 บิต

  • ptr32 ชี้ไปยัง machine word ขนาด 32 บิต

แนวคิดของพอยน์เตอร์อาจไม่คุ้นเคยสำหรับนักเขียนโปรแกรม Python มันมีความคล้ายคลึงกับออบเจกต์ memoryview ของ Python ตรงที่มันให้การเข้าถึงโดยตรงไปยังข้อมูลที่เก็บอยู่ในหน่วยความจำ ไอเทมถูกเข้าถึงโดยใช้ subscript notation แต่ไม่รองรับ slice: พอยน์เตอร์สามารถส่งคืนไอเทมเดียวเท่านั้น จุดประสงค์คือเพื่อให้การเข้าถึงข้อมูลแบบสุ่มอย่างรวดเร็วไปยังข้อมูลที่เก็บอยู่ในตำแหน่งหน่วยความจำที่ต่อเนื่องกัน เช่น ข้อมูลที่เก็บอยู่ในออบเจกต์ที่รองรับ buffer protocol และ memory-mapped peripheral registers ใน microcontroller ควรทราบว่าการเขียนโปรแกรมโดยใช้พอยน์เตอร์เป็นสิ่งอันตราย: ไม่มีการตรวจสอบขอบเขตและคอมไพเลอร์ไม่ทำอะไรเพื่อป้องกันข้อผิดพลาด buffer overrun

การใช้งานทั่วไปคือการแคชตัวแปร:

@micropython.viper
def foo(self, arg: int) -> int:
    buf = ptr8(self.linebuf) # self.linebuf is a bytearray or bytes object
    for x in range(20, 30):
        bar = buf[x] # Access a data item through the pointer
        # code omitted

ในกรณีนี้คอมไพเลอร์ "รู้" ว่า buf เป็นที่อยู่ของอาร์เรย์ไบต์ มันสามารถส่งออกโค้ดเพื่อคำนวณที่อยู่ของ buf[x] ขณะรันไทม์ได้อย่างรวดเร็ว เมื่อใช้ cast เพื่อแปลงออบเจกต์เป็นประเภท native ของ Viper ควรทำที่จุดเริ่มต้นของฟังก์ชันมากกว่าใน timing loops ที่สำคัญ เนื่องจากการดำเนินการ cast อาจใช้เวลาหลายไมโครวินาที กฎสำหรับการ cast มีดังนี้:

  • ตัวดำเนินการ cast ในปัจจุบันคือ: int, bool, uint, ptr, ptr8, ptr16 และ ptr32

  • ผลลัพธ์ของ cast จะเป็นตัวแปร native Viper

  • อาร์กิวเมนต์ของ cast สามารถเป็นออบเจกต์ Python หรือตัวแปร native Viper

  • หากอาร์กิวเมนต์เป็นตัวแปร native Viper การ cast จะเป็น no-op (กล่าวคือ ไม่เสียค่าใช้จ่ายขณะรันไทม์) ซึ่งแค่เปลี่ยนประเภท (เช่น จาก uint เป็น ptr8) เพื่อให้คุณสามารถจัดเก็บ/โหลดโดยใช้พอยน์เตอร์นี้

  • หากอาร์กิวเมนต์เป็นออบเจกต์ Python และ cast เป็น int หรือ uint ออบเจกต์ Python จะต้องเป็นประเภทจำนวนเต็มและค่าของออบเจกต์จำนวนเต็มนั้นจะถูกส่งกลับ

  • อาร์กิวเมนต์ของ bool cast ต้องเป็นประเภทจำนวนเต็ม (boolean หรือ integer) เมื่อใช้เป็นประเภทที่ส่งกลับ ฟังก์ชัน viper จะส่งคืนออบเจกต์ True หรือ False

  • หากอาร์กิวเมนต์เป็นออบเจกต์ Python และ cast เป็น ptr, ptr8, ptr16 หรือ ptr32 ออบเจกต์ Python จะต้องมี buffer protocol (ในกรณีนี้จะส่งคืนพอยน์เตอร์ไปยังจุดเริ่มต้นของบัฟเฟอร์) หรือต้องเป็นประเภทจำนวนเต็ม (ในกรณีนี้จะส่งคืนค่าของออบเจกต์จำนวนเต็มนั้น)

การเขียนไปยังพอยน์เตอร์ที่ชี้ไปยังออบเจกต์แบบอ่านอย่างเดียวจะนำไปสู่พฤติกรรมที่ไม่กำหนด

Note

ตัวอย่างโค้ดด้านล่างนี้ให้ไว้สำหรับ OpenMV Cams ที่ใช้ STM32 ซึ่งมีโมดูล stm เทคนิคที่อธิบายไว้ใช้ได้โดยทั่วไป

โมดูล stm เปิดเผยที่อยู่หน่วยความจำของ peripheral registers ของ MCU แต่ละพอร์ต GPIO มี output data register (ODR) ซึ่งบิตจะแมปแบบหนึ่งต่อหนึ่งกับพินของพอร์ตนั้น: การเขียน register จะขับพินเหล่านั้นโดยตรง โดยไม่มีค่าใช้จ่ายจากการเรียกเมธอด machine.Pin และ XOR บิตจะสลับพินนั้น บน OpenMV Cam ดั้งเดิม LED สีน้ำเงินถูกต่อสายกับพิน 2 ของ GPIOC ดังนั้นตัวอย่างต่อไปนี้ใช้ cast ptr16 เพื่อสลับ LED สีน้ำเงิน n ครั้ง:

BIT2 = const(1 << 2)
@micropython.viper
def toggle_n(n: int):
    odr = ptr16(stm.GPIOC + stm.GPIO_ODR)
    for _ in range(n):
        odr[0] ^= BIT2

คำอธิบายทางเทคนิคโดยละเอียดของ code emitter ทั้งสามสามารถพบได้บน Kickstarter ที่นี่ Note 1 และที่นี่ Note 2

การเข้าถึงฮาร์ดแวร์โดยตรง

หัวข้อนี้อยู่ในหมวดหมู่ของการเขียนโปรแกรมขั้นสูงและต้องมีความรู้เกี่ยวกับ MCU เป้าหมายบ้าง พิจารณาตัวอย่างการสลับพิน output บน OpenMV Cam วิธีมาตรฐานจะเขียน

mypin.value(mypin.value() ^ 1) # mypin was instantiated as an output pin

ซึ่งเกี่ยวข้องกับค่าใช้จ่ายของการเรียกเมธอด value() ของอินสแตนซ์ Pin สองครั้ง ค่าใช้จ่ายนี้สามารถกำจัดได้โดยการอ่าน/เขียนบิตที่เกี่ยวข้องของ GPIO port output data register (ODR) ของชิป เพื่ออำนวยความสะดวกในเรื่องนี้ โมดูล stm มีชุดค่าคงที่ที่ให้ที่อยู่ของ registers ที่เกี่ยวข้อง (stm.GPIOC คือที่อยู่ฐานของพอร์ต GPIOC, stm.GPIO_ODR คือ offset ของ output data register) ดังที่กล่าวไว้ข้างต้น LED สีน้ำเงินบน OpenMV Cam ดั้งเดิมคือพิน 2 ของ GPIOC ดังนั้นการสลับอย่างรวดเร็วสามารถทำได้ดังนี้:

import machine
import stm

BIT2 = const(1 << 2)
machine.mem16[stm.GPIOC + stm.GPIO_ODR] ^= BIT2