2.40. זיכרון ואיסוף אשפה

MicroPython מנהל זיכרון באותה דרך ש-CPython עושה זאת: כל אובייקט חי על ה-ערימה (heap), ו-אוסף אשפה (garbage collector, GC) משחרר אובייקטים ששום דבר אינו מפנה אליהם עוד. במכשיר עם כמה מאות קילובייט של RAM, תשומת לב לאופן שבו הערימה נמצאת בשימוש נחוצה מדי פעם; ב-Python שולחני, היא נחוצה לעיתים נדירות.

2.40.1. הערימה

הערימה היא אזור ה-RAM – ברוב מצלמות OpenMV הוא למעשה מפוצל בין יותר ממאגר זיכרון פיזי אחד – שזמן הריצה מחלק לאובייקטים של Python. בכל פעם שביטוי Python יוצר אובייקט חדש (רשימה, מחרוזת, dict, tuple, כל דבר שאינו מספר שלם קטן או singleton), בלוק של בייטים יוצא מהערימה כדי להחזיק אותו. כאשר ה-GC מבחין שאובייקט אינו ניתן להגעה, הוא מחזיר את הבלוק למאגר הפנוי שממנו הוא בא.

שתי פונקציות במודול gc שכדאי להכיר:

  • gc.mem_free() – מספר משוער של בייטים פנויים על הערימה כעת.

  • gc.collect() – מריצה מחזור איסוף מיד במקום להמתין שזמן הריצה יפעיל אחד.

import gc

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

המספרים המדויקים תלויים בגרסת הבנייה; הכיוון של השינוי הוא מה שחשוב: הקצאת דברים גדולים מקטינה את mem_free, וזריקת ההפניות בתוספת gc.collect מחזירה את הערימה.

2.40.2. פיצול (Fragmentation)

המאגר הפנוי אינו רציף בקסם. ככל שאובייקטים באים והולכים באורכי חיים שונים, המרחב הפנוי מתפצל לחלקים קטנים וקטנים יותר אפילו כשגודלו הכולל עדיין גדול. הקצאת אובייקט גדול מהחלק הפנוי היחיד הגדול ביותר נכשלת עם 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 פנוי כולל יכול להחזיק חוצץ (buffer) גדול (משמאל) או לסרב לכך (מימין) בהתאם למידת הפיצול שלו.

פיצול הוא בעיקר דאגה בסקריפטים ארוכי-ריצה שממשיכים להקצות ולשחרר חוצצים (buffers) בגדלים שונים. ההפחתה היעילה ביותר היחידה היא להקצות מראש חוצצים ארוכי-חיים סמוך לתחילת התוכנית, לפני שלהקצאות קצרות-חיים רבות הייתה הזדמנות לפזר אותם.

2.40.3. הקצאה מראש

שני דפוסים גורמים לערימה להתנהג כראוי:

  • הקצו חוצצים בגודל קבוע פעם אחת ועשו בהם שימוש חוזר, במקום לבנות רשימה חדשה או 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() בדרך כלל אוטומטית – זמן הריצה מפעיל אותה כשהקצאות אינן יכולות למצוא מספיק זיכרון פנוי. קריאה אליה ידנית שימושית בשני מצבים:

  • מיד לאחר שאצווה גדולה של אובייקטים יצאה מהתחום, כדי לשחרר אותם מיד במקום להמתין שההקצאה הבאה תשלם את העלות.

  • מיד לפני קטע הזקוק לכמות מרבית ידועה של זיכרון פנוי, כדי להימנע מכך שה-GC יופעל באמצע פעולה רגישה לזמן.

פיזור קריאות gc.collect בכל מקום אינו הופך תוכנית למהירה יותר – האיסוף עצמו לוקח זמן. השתמשו בה במכוון, בנקודות שבהן העלות של איסוף לא מתוזמן תהיה גרועה יותר מהעלות של אחד שהרצתם בכוונה.