2.40. หน่วยความจำและการเก็บขยะ

MicroPython จัดการหน่วยความจำในลักษณะเดียวกับ CPython: ทุกอ็อบเจ็กต์อาศัยอยู่บน heap และ garbage collector (GC) จะปลดปล่อยอ็อบเจ็กต์ที่ไม่มีอะไรอ้างอิงอีกต่อไป บนอุปกรณ์ที่มี RAM ไม่กี่ร้อยกิโลไบต์ การใส่ใจการใช้งาน heap บางครั้งมีความจำเป็น; บน Python สำหรับเดสก์ท็อปแทบไม่จำเป็นเลย

2.40.1. Heap

Heap คือพื้นที่ RAM -- บน OpenMV cameras ส่วนใหญ่จริงๆ แล้วแบ่งออกระหว่างมากกว่าหนึ่ง memory pool ทางกายภาพ -- ที่ runtime ส่งมอบให้กับอ็อบเจ็กต์ Python ทุกครั้งที่นิพจน์ Python สร้างอ็อบเจ็กต์ใหม่ (list, string, dict, tuple, อะไรก็ตามที่ไม่ใช่จำนวนเต็มขนาดเล็กหรือ singleton) บล็อกของไบต์จะออกมาจาก heap เพื่อเก็บมัน เมื่อ GC สังเกตว่าอ็อบเจ็กต์ไม่สามารถเข้าถึงได้ มันจะคืนบล็อกไปยัง free pool ที่มันมาจาก

ฟังก์ชันสองตัวในโมดูล gc ที่ควรรู้จัก:

  • gc.mem_free() -- จำนวนไบต์ว่างโดยประมาณบน heap ในขณะนี้

  • gc.collect() -- รันรอบการเก็บทันทีแทนที่จะรอให้ runtime ทริกเกอร์

import gc

print("before:", gc.mem_free())
big = [0] * 10000
print("after :", gc.mem_free())
del big
gc.collect()
print("freed :", gc.mem_free())

ตัวเลขที่แน่นอนขึ้นอยู่กับ build; ทิศทาง ของการเปลี่ยนแปลงคือสิ่งที่สำคัญ: การจัดสรรสิ่งใหญ่จะลด mem_free และการละทิ้งการอ้างอิงพร้อมกับ gc.collect คืน heap กลับมา

2.40.2. การแตกกระจาย

Free pool ไม่ได้อยู่ติดกันอย่างมหัศจรรย์ เมื่ออ็อบเจ็กต์มาและไปตามอายุการใช้งานที่แตกต่างกัน พื้นที่ว่างแตกออกเป็นชิ้นที่เล็กลงเรื่อยๆ แม้ขนาดรวมยังคงใหญ่อยู่ การจัดสรรอ็อบเจ็กต์ที่ใหญ่กว่าชิ้นว่างเดียวที่ใหญ่ที่สุดจะล้มเหลวด้วย MemoryError -- แม้ว่าจะมี RAM ว่างรวมกันเพียงพอในทางเทคนิค

Two views of the heap. Before: one large contiguous free area. After: many small free areas interleaved with allocated blocks, none individually large enough for a big allocation.

RAM ว่างรวมเท่ากันสามารถรองรับบัฟเฟอร์ขนาดใหญ่ (ซ้าย) หรือปฏิเสธไม่ได้ (ขวา) ขึ้นอยู่กับว่ามันแตกกระจายมากแค่ไหน

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

2.40.3. การจัดสรรล่วงหน้า

สองแนวทางทำให้ heap ทำงานได้ดี:

  • จัดสรรบัฟเฟอร์ขนาดคงที่ครั้งเดียวและนำกลับมาใช้ใหม่ แทนที่จะสร้าง list หรือ bytearray ใหม่ต่อการวนซ้ำ

  • ดึงค่าคงที่และตารางค้นหาออกจากลูปชั้นใน เพื่อให้สร้างเพียงครั้งเดียว

buf = bytearray(64)        # one allocation, reused below

def fill(value):
    for i in range(len(buf)):
        buf[i] = value

fill(0)
fill(255)

เปรียบเทียบกับเวอร์ชันที่สร้าง bytearray ใหม่ภายในลูป: การวนซ้ำแต่ละครั้งสร้างขยะที่ GC ต้องทำความสะอาดในภายหลัง เวอร์ชันที่จัดสรรล่วงหน้าไม่สร้างขยะเลย

2.40.4. เมื่อใดควรเรียก gc.collect

gc.collect() ปกติจะทำงานอัตโนมัติ -- runtime ทริกเกอร์เมื่อการจัดสรรหาหน่วยความจำว่างไม่พอ การเรียกด้วยมือมีประโยชน์ในสองสถานการณ์:

  • ทันทีหลังจากที่อ็อบเจ็กต์จำนวนมากออกนอกขอบเขต เพื่อปลดปล่อยทันทีแทนที่จะรอให้การจัดสรรครั้งถัดไปต้องรับภาระ

  • ก่อนส่วนที่ต้องการหน่วยความจำว่างสูงสุดที่ทราบแน่นอน เพื่อหลีกเลี่ยง GC ที่ยิงขึ้นระหว่างการดำเนินการที่ต้องการเวลาที่แน่นอน

การโรยคำสั่ง gc.collect ทุกที่ไม่ได้ทำให้โปรแกรมเร็วขึ้น -- การเก็บขยะเองต้องใช้เวลา ใช้มันอย่างจงใจ ที่จุดซึ่งต้นทุนของการเก็บที่ไม่ได้กำหนดเวลาจะแย่กว่าต้นทุนของการที่คุณรันด้วยตัวเอง