การเพิ่มความเร็วสูงสุดใน 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