מיזוג מחרוזות (interning) ב-MicroPython¶
MicroPython משתמש ב-string interning כדי לחסוך גם ב-RAM וגם ב-ROM. כך נמנע הצורך לאחסן עותקים כפולים של אותה מחרוזת. בעיקר, הדבר חל על מזהים בקוד שלך, מכיוון שדבר כמו שם של פונקציה או משתנה צפוי מאוד להופיע במקומות רבים בקוד. ב-MicroPython מחרוזת ממוזגת נקראת QSTR (uniQue STRing).
ערך QSTR (מהטיפוס qstr) הוא אינדקס לתוך רשימה מקושרת של מאגרי (pools) QSTR. מבני QSTR מאחסנים את אורכם ו-hash של תוכנם לצורך השוואה מהירה במהלך תהליך הסרת הכפילויות. כל פעולות ה-bytecode העובדות עם מחרוזות משתמשות בארגומנט QSTR.
יצירת QSTR בזמן הידור (compile-time)¶
בקוד ה-C של MicroPython, כל מחרוזת שאמורה להתמזג (interned) בקושחה הסופית נכתבת בצורת MP_QSTR_Foo. בזמן ההידור זה יוערך לערך qstr המצביע על האינדקס של "Foo" במאגר ה-QSTR.
תהליך רב-שלבי ב-Makefile גורם לכך לעבוד. בתמצית, לתהליך זה שלושה חלקים:
מציאת כל הטוקנים
MP_QSTR_Fooבקוד.יצירת מאגר QSTR סטטי המכיל את כל נתוני המחרוזות (כולל אורכים ו-hashes).
החלפת כל
MP_QSTR_Foo(באמצעות המעבד המקדים) באינדקס המתאים שלהם.
הטוקנים MP_QSTR_Foo מחופשים בשני מקורות:
כל הקבצים המופנים אליהם ב-
$(SRC_QSTR). זהו כל קוד ה-C (כלומרpy,extmod,ports/stm32) אך לא כולל קוד צד שלישי כגוןlib.תוספת של
$(QSTR_GLOBAL_DEPENDENCIES)(הכוללת אתmpconfig*.h).
הערה: ל-frozen_mpy.c (שנוצר על ידי mpy-tool.py) יש יצירת QSTR ומאגר משלו.
מספר מחרוזות נוספות שלא ניתן לבטא באמצעות התחביר MP_QSTR_Foo (למשל הן מכילות תווים שאינם אלפאנומריים) מסופקות במפורש ב-qstrdefs.h וב-qstrdefsport.h באמצעות המשתנה $(QSTR_DEFS).
העיבוד מתרחש בשלבים הבאים:
qstr.i.lastהוא שרשור של העברת כל קובץ קלט בודד דרך המעבד המקדים של C. משמעות הדבר היא שכל קוד שהושבת באופן מותנה יוסר, ומאקרו יורחבו. משמעות הדבר היא שאיננו מוסיפים למאגר מחרוזות שלא ישמשו בקושחה הסופית. מכיוון שבשלב זה (הודות למאקרוNO_QSTRשנוסף על ידיQSTR_GEN_CFLAGS) אין הגדרה ל-MP_QSTR_Foo, הוא עובר דרך שלב זה ללא שינוי. קובץ זה כולל גם הערות מהמעבד המקדים הכוללות מידע על מספרי שורות. שים לב ששלב זה משתמש רק בקבצים ששונו, כלומרqstr.i.lastיכיל רק נתונים מקבצים ששונו מאז ההידור האחרון.qstr.splitהוא קובץ ריק הנוצר לאחר הרצתmakeqstrdefs.py splitעל qstr.i.last. הוא משמש פשוט כתלות (dependency) כדי לציין שהשלב רץ. הסקריפט הזה מפיק קובץ אחד לכל קובץ C קלט,genhdr/qstr/...file.c.qstr, המכיל אך ורק את ה-QSTR שהותאמו. כל QSTR מודפס בצורתQ(Foo). שלב זה נחוץ כדי לשלב את הקבצים הקיימים עם הנתונים החדשים שנוצרו מהעדכון ההדרגתי ב-qstr.i.last.qstrdefs.collected.hהוא הפלט של שרשורgenhdr/qstr/*באמצעותmakeqstrdefs.py cat. זוהי כעת הקבוצה המלאה של ה-MP_QSTR_Fooשנמצאו בקוד, כעת בפורמטQ(Foo), אחד לכל שורה, עם כפילויות. קובץ זה מתעדכן רק אם קבוצת ה-qstr השתנתה. hash של נתוני ה-QSTR נכתב לקובץ אחר (qstrdefs.collected.h.hash) המאפשר לעקוב אחר שינויים בין בנייה (build) לבנייה.יצירת מנייה (enumeration), שכל ערך בה ממפה
MP_QSTR_Fooלאינדקס המתאים שלו. היא משרשרת אתqstrdefs.collected.hעםqstrdefs*.h, ואז ממירה כל שורה מ-Q(Foo)ל-"Q(Foo)"כך שיעברו דרך המעבד המקדים ללא שינוי. לאחר מכן המעבד המקדים משמש לטיפול בכל הידור מותנה ב-qstrdefs*.h. אז ההמרה מבוטלת בחזרה ל-Q(Foo), ונשמרת כ-qstrdefs.preprocessed.h.qstrdefs.generated.hהוא הפלט שלmakeqstrdata.py. עבור כלQ(Foo)ב-qstrdefs.preprocessed.h (בתוספת כמה ערכים נוספים מקודדים-קשיח), הוא מפיקQDEF(MP_QSTR_Foo, (const byte*)"hash" "Foo").
לאחר מכן, בהידור הראשי, שני דברים קורים עם qstrdefs.generated.h:
ב-qstr.h, כל QDEF הופך לערך ב-enum, מה שהופך את
MP_QSTR_Fooלזמין לקוד ושווה לאינדקס של אותה מחרוזת בטבלת ה-QSTR.ב-qstr.c, טבלת נתוני ה-QSTR בפועל נוצרת כאיברים של
mp_qstr_const_pool->qstrs.
יצירת QSTR בזמן ריצה (run-time)¶
ניתן ליצור מאגרי QSTR נוספים בזמן ריצה כך שניתן להוסיף אליהם מחרוזות. לדוגמה, הקוד:
foo[x] = 3
יידרש ליצור QSTR עבור הערך של x כדי שניתן יהיה להשתמש בו על ידי ה-bytecode ”load attr“.
כמו כן, בעת הידור קוד Python, יש ליצור QSTR עבור מזהים וליטרלים. הערה: רק ליטרלים קצרים מ-10 תווים הופכים ל-QSTR. זאת מכיוון שמחרוזת רגילה ב-heap תופסת תמיד מינימום של 16 בייטים (בלוק GC אחד), בעוד ש-QSTR מאפשרים לארוז אותם ביעילות רבה יותר בתוך המאגר.
מאגרי QSTR (וה“נתחים“ (chunks) שבבסיסם המאחסנים את נתוני המחרוזות) מוקצים לפי דרישה ב-heap בגודל מינימלי.