תאריך פרסום: 29 בינואר 2025
איסוף אשפה ב-WebAssembly (WasmGC)
יש שני סוגים של שפות תכנות: שפות תכנות עם איסוף אשפה ושפות תכנות שדורשות ניהול ידני של זיכרון. דוגמאות לקודים מהסוג הראשון, בין היתר, הן Kotlin, PHP או Java. דוגמאות לקודים כאלה הן C, C++ או Rust. ככלל, בשפות תכנות ברמה גבוהה יותר יש סיכוי גבוה יותר לכך שאיסוף אשפה יהיה תכונה סטנדרטית.
במילים פשוטות, איסוף אשפה הוא ניסיון לנצל מחדש זיכרון שהוקצה על ידי התוכנית, אבל כבר אין אליו הפניה. זיכרון כזה נקרא. יש הרבה שיטות להטמעת איסוף אשפה. אחת השיטות הקלות ביותר להבנה היא ספירת הפניות, שבה המטרה היא לספור את מספר הפניות לאובייקטים בזיכרון.
יכול להיות שזה נשמע כמו התחלה של רעיון, אבל שפות תכנות מיושמות בשפות תכנות אחרות. לדוגמה, סביבת זמן הריצה של PHP מוטמעת בעיקר ב-C. אם מפתחים רוצים לקמפל שפה כמו PHP ל-Wasm, בדרך כלל הם צריכים לקמפל את כל החלקים, כמו המנתח של השפה, תמיכה בספריות, איסוף אשפה ורכיבים חיוניים אחרים.
Wasm פועל בדפדפן בהקשר של שפת המארח JavaScript. ב-Chrome, JavaScript ו-Wasm פועלים ב-V8, מנוע JavaScript של Google בקוד פתוח. ול-V8 כבר יש אספן אשפה. כלומר, מפתחים שמשתמשים, לדוגמה, ב-PHP שעבר הידור ל-Wasm, בסופו של דבר שולחים לדפדפן הטמעה של מנקה האשפה של השפה שהועברה (PHP), שכבר יש לו מנקה אשפה. זה בזבוז זמן, כפי שזה נשמע. כאן נכנס WasmGC לתמונה.
מידע נוסף על WasmGC זמין במאמר WebAssembly Garbage Collection (WasmGC) מופעל עכשיו כברירת מחדל ב-Chrome. אם אתם רוצים להבין את הנושא לעומק, כדאי לקרוא את הפוסט בבלוג של V8 בנושא דרך חדשה להביא שפות תכנות עם איסוף אשפה ל-WebAssembly ביעילות.
אופטימיזציות של קריאה חוזרת לקוד בסוף (tail call) ב-Wasm
קריאה נמצאת במיקום זנב אם היא ההוראה האחרונה שמופעלת לפני החזרה מהפונקציה הנוכחית. ניתן לבצע אופטימיזציה של קריאות כאלה על ידי ביטול המסגרת של מבצע הקריאה והחלפת הקריאה בקפיצה. האפשרות הזו שימושית במיוחד לפונקציות רפליקטיביות. לדוגמה, פונקציית C הזו שמסכמת את הרכיבים של רשימת קישורים:
int sum(List* list, int acc) {
if (list == nullptr) return acc;
return sum(list->next, acc + list->val);
}
בקריאה רגילה, הפעולה הזו צורכת מקום בסטאק לפי O(n): כל רכיב ברשימה מוסיף מסגרת חדשה לסטאק הקריאות. אם הרשימה ארוכה מספיק, זה עלול לגרום לחריגה מהמגבלה של ה-stack במהירות רבה. החלפת הקריאה בקפיצה הופכת את הפונקציה הרקורסיבית הזו למעגל שמשתמש במרחב סטאק של O(1):
int sum(List* list, int acc) {
while (list != nullptr) {
acc = acc + list->val;
list = list->next;
}
return acc;
}
האופטימיזציה הזו חשובה במיוחד בשפות פונקציונליות. הן מסתמכות במידה רבה על פונקציות רפליקטיביות, ובשפות טהורות כמו Haskell אין אפילו מבני בקרה של לולאות. כל סוג של איטרציה מותאמת אישית כולל בדרך כלל חזרה חוזרת (recursion) בדרך כזו או אחרת. ללא אופטימיזציה של קריאה לקוד בסוף, כל תוכנית לא טריוויאלית תגיע במהירות לחוסר מקום בסטאק, כי אחרת נגמר במהירות המקום בסטאק.
בהתחלה, WebAssembly לא איפשר אופטימיזציות כאלה של קריאה חוזרת לקוד בסוף (tail call), אבל המצב השתנה עם הצעה להרחבה של קריאה חוזרת לקוד בסוף. מידע נוסף זמין במאמר WebAssembly tail calls בבלוג V8.
מסקנות
עכשיו, כש-WasmGC ואופטימיזציות של קריאה חוזרת לקודם (tail call) זמינות ב-Baseline, אפליקציות נוספות יכולות להשתמש בתכונות האלה כדי לשפר את הביצועים. למשל, העברנו את העובד לחישוב של Google Sheets ל-WasmGC.