יש שני סוגים של שפות תכנות: שפות תכנות עם איסוף אשפה ושפות תכנות שדורשות ניהול זיכרון ידני. דוגמאות לשפות מהסוג הראשון, מתוך רבות אחרות, הן Kotlin, PHP או Java. דוגמאות לשפות כאלה הן C, C++ או Rust. ככלל, בשפות תכנות ברמה גבוהה יותר יש סיכוי גבוה יותר שאיסוף אשפה יהיה תכונה סטנדרטית. בפוסט הזה בבלוג, נתמקד בשפות תכנות כאלה עם איסוף אשפה, ונסביר איך אפשר לקמפל אותן ל-WebAssembly (Wasm). אבל מה זה בכלל איסוף אשפה (Garbage Collection, GC)?
Browser Support
איסוף אשפה
במילים פשוטות, הרעיון של איסוף אשפה הוא ניסיון לשחרר זיכרון שהוקצה על ידי התוכנית, אבל לא מתבצעת אליו יותר הפניה. הזיכרון הזה נקרא garbage (). יש הרבה שיטות להטמעה של איסוף אשפה. אחת מהן היא ספירת הפניות, שבה המטרה היא לספור את מספר ההפניות לאובייקטים בזיכרון. כאשר אין יותר הפניות לחפץ, ניתן לסמן אותו כלא בשימוש ובכך מוכן לאיסוף אשפה. מנגנון איסוף האשפה של PHP משתמש בספירת הפניות, והשימוש בפונקציה xdebug_debug_zval()
של התוסף Xdebug מאפשר לכם להציץ מתחת למכסה. נניח שיש לנו את תוכנית ה-PHP הבאה.
<?php
$a= (string) rand();
$c = $b = $a;
$b = 42;
unset($c);
$a = null;
?>
התוכנית מקצה מספר אקראי שמומר למחרוזת למשתנה חדש שנקרא a
. לאחר מכן נוצרים שני משתנים חדשים, b
ו-c
, ומוקצה להם הערך a
. לאחר מכן, המערכת מקצה מחדש את b
למספר 42
, ומבטלת את ההגדרה של c
. לבסוף, הוא מגדיר את הערך של a
ל-null
. אם מוסיפים הערות לכל שלב בתוכנית עם xdebug_debug_zval()
, אפשר לראות את מונה ההפניות של איסוף האשפה בפעולה.
<?php
$a= (string) rand();
$c = $b = $a;
xdebug_debug_zval('a');
$b = 42;
xdebug_debug_zval('a');
unset($c);
xdebug_debug_zval('a');
$a = null;
xdebug_debug_zval('a');
?>
בדוגמה שלמעלה, הפלט יהיה היומנים הבאים, שבהם אפשר לראות איך מספר ההפניות לערך של המשתנה a
יורד אחרי כל שלב, וזה הגיוני בהתחשב ברצף הקוד. (המספר האקראי שלכם יהיה שונה, כמובן).
a:
(refcount=3, is_ref=0)string '419796578' (length=9)
a:
(refcount=2, is_ref=0)string '419796578' (length=9)
a:
(refcount=1, is_ref=0)string '419796578' (length=9)
a:
(refcount=0, is_ref=0)null
יש אתגרים נוספים שקשורים לאיסוף אשפה, כמו זיהוי מחזורים, אבל לצורך המאמר הזה, מספיק להבין את הבסיס של ספירת הפניות.
שפות תכנות מיושמות בשפות תכנות אחרות
יכול להיות שזה נשמע כמו סרט, אבל שפות תכנות מיושמות בשפות תכנות אחרות. לדוגמה, זמן הריצה של PHP מיושם בעיקר ב-C. אפשר לעיין בקוד המקור של PHP ב-GitHub. קוד ה-garbage collection של PHP נמצא בעיקר בקובץ zend_gc.c
. רוב המפתחים יתקינו את PHP דרך מנהל החבילות של מערכת ההפעלה שלהם. אבל מפתחים יכולים גם ליצור PHP מקוד המקור. לדוגמה, בסביבת Linux, השלבים ./buildconf && ./configure && make
ייצרו PHP עבור זמן הריצה של Linux. אבל זה גם אומר שאפשר לקמפל את זמן הריצה של PHP לזמני ריצה אחרים, כמו Wasm.
שיטות מסורתיות להעברת שפות לזמן הריצה של Wasm
בלי קשר לפלטפורמה שבה PHP פועל, סקריפטים של PHP עוברים קומפילציה לאותו קוד בייט ומופעלים על ידי Zend Engine. Zend Engine הוא מהדר וסביבת זמן ריצה לשפת הסקריפט PHP. הוא מורכב מהמכונה הווירטואלית (VM) של Zend, שכוללת את Zend Compiler ואת Zend Executor. שפות כמו PHP שמיושמות בשפות אחרות ברמה גבוהה, כמו C, כוללות בדרך כלל אופטימיזציות שמיועדות לארכיטקטורות ספציפיות, כמו Intel או ARM, ודורשות קצה עורפי שונה לכל ארכיטקטורה. בהקשר הזה, Wasm מייצג ארכיטקטורה חדשה. אם למכונה הווירטואלית יש קוד ספציפי לארכיטקטורה, כמו הידור בזמן ריצה (JIT) או הידור מראש (AOT), המפתח מטמיע גם קצה עורפי (backend) ל-JIT/AOT עבור הארכיטקטורה החדשה. הגישה הזו הגיונית מאוד, כי לרוב אפשר פשוט לקמפל מחדש את החלק העיקרי של בסיס הקוד לכל ארכיטקטורה חדשה.
בהתחשב בכך ש-Wasm היא שפה ברמה נמוכה, טבעי לנסות את אותה גישה גם שם: להדר מחדש את קוד ה-VM הראשי עם מנתח התקנים, תמיכה בספרייה, איסוף אשפה ואופטימיזציה ל-Wasm, ולהטמיע קצה עורפי של JIT או AOT ל-Wasm אם צריך. האפשרות הזו קיימת מאז ה-MVP של Wasm, ובפועל היא עובדת טוב במקרים רבים. למעשה, PHP שעבר קומפילציה ל-Wasm הוא מה שמפעיל את WordPress Playground. מידע נוסף על הפרויקט זמין במאמר Build in-browser WordPress experiences with WordPress Playground and WebAssembly (בניית חוויות WordPress בדפדפן באמצעות WordPress Playground ו-WebAssembly).
עם זאת, PHP Wasm פועל בדפדפן בהקשר של שפת המארח JavaScript. ב-Chrome, JavaScript ו-Wasm מורצים ב-V8, מנוע JavaScript של Google בקוד פתוח שמיישם ECMAScript כפי שצוין ב-ECMA-262. ול- V8 כבר יש אספן אשפה. המשמעות היא שמפתחים שמשתמשים, למשל, ב-PHP שעבר קומפילציה ל-Wasm, בסופו של דבר שולחים לדפדפן הטמעה של איסוף אשפה בשפה שעברה המרה (PHP), כשהדפדפן כבר כולל איסוף אשפה. זה בזבוז משאבים כמו שזה נשמע. כאן נכנס לתמונה WasmGC.
בעיה נוספת בגישה הישנה של מתן אפשרות למודולי Wasm לבנות את איסוף האשפה שלהם על גבי הזיכרון הליניארי של Wasm היא שאין אינטראקציה בין איסוף האשפה של Wasm לבין איסוף האשפה של השפה שעברה קומפילציה ל-Wasm, שנבנה על גביו. מצב כזה עלול לגרום לבעיות כמו דליפות זיכרון וניסיונות לא יעילים לאיסוף אשפה. כדי להימנע מהבעיות האלה, מודולי Wasm יכולים לעשות שימוש חוזר ב-GC המובנה הקיים.
העברת שפות תכנות לסביבות ריצה חדשות באמצעות WasmGC
WasmGC היא הצעה של קבוצת הקהילה של WebAssembly. ההטמעה הנוכחית של Wasm MVP יכולה להתמודד רק עם מספרים, כלומר עם מספרים שלמים ומספרים עשרוניים, בזיכרון ליניארי. עם זאת, עם ההצעה לשימוש בסוגי הפניות שנשלחת, Wasm יכול גם להחזיק הפניות חיצוניות. WasmGC מוסיף עכשיו סוגי ערימה של struct ו-array, מה שאומר שיש תמיכה בהקצאת זיכרון לא ליניארית. לכל אובייקט WasmGC יש סוג ומבנה קבועים, ולכן קל למכונות וירטואליות ליצור קוד יעיל כדי לגשת לשדות שלהן בלי הסיכון לביטול אופטימיזציות שקיים בשפות דינמיות כמו JavaScript. לכן, ההצעה הזו מוסיפה ל-WebAssembly תמיכה יעילה בשפות מנוהלות ברמה גבוהה, באמצעות סוגי ערימה של מבנים ומערכים שמאפשרים לקומפיילרים של שפות שמטרגטים את Wasm להשתלב עם איסוף אשפה במכונה הווירטואלית המארחת. במילים פשוטות, המשמעות היא שעם WasmGC, כשמעבירים שפת תכנות ל-Wasm, כבר לא צריך לכלול את איסוף האשפה של שפת התכנות בהעברה, אלא אפשר להשתמש באיסוף האשפה הקיים.
כדי לאמת את ההשפעה של השיפור הזה בעולם האמיתי, צוות Wasm של Chrome הרכיב גרסאות של המדד Fannkuch (שמקצה מבני נתונים בזמן שהוא פועל) מ-C, Rust ו-Java. הקובץ הבינארי של C ו-Rust יכול להיות בגודל של 6.1K עד 9.6K, בהתאם לדגלי הקומפיילר השונים, בעוד שהגרסה של Java קטנה בהרבה – רק 2.3K! שפות C ו-Rust לא כוללות איסוף אשפה, אבל הן עדיין כוללות את malloc/free
לניהול הזיכרון. הסיבה לכך ש-Java קטנה יותר היא שהיא לא צריכה לכלול קוד לניהול זיכרון בכלל. זו רק דוגמה ספציפית אחת, אבל היא מראה שלבינאריים של WasmGC יש פוטנציאל להיות קטנים מאוד, וזה עוד לפני שנעשתה עבודה משמעותית על אופטימיזציה של הגודל.
צפייה בשפת תכנות שהועברה ל-WasmGC בפעולה
Kotlin Wasm
אחת משפות התכנות הראשונות שעברו המרה ל-Wasm בזכות WasmGC היא Kotlin בצורה של Kotlin/Wasm. ההדגמה, עם קוד המקור באדיבות צוות Kotlin, מוצגת ברשימה הבאה.
import kotlinx.browser.document
import kotlinx.dom.appendText
import org.w3c.dom.HTMLDivElement
fun main() {
(document.getElementById("warning") as HTMLDivElement).style.display = "none"
document.body?.appendText("Hello, ${greet()}!")
}
fun greet() = "world"
אולי אתם תוהים מה הטעם, כי קוד Kotlin שלמעלה מורכב בעצם מממשקי API של JavaScript OM שהומרו ל-Kotlin. השימוש ב-Compose Desktop מתחיל להיות הגיוני יותר בשילוב עם Compose Multiplatform, שמאפשר למפתחים לבנות על ממשק המשתמש שהם כבר יצרו לאפליקציות Android Kotlin שלהם. אפשר לראות ניסיון מוקדם של זה בכלי לצפייה בתמונות Kotlin/Wasm, שגם הוא פותח על ידי צוות Kotlin.
Dart ו-Flutter
צוותי Dart ו-Flutter ב-Google גם מכינים תמיכה ב-WasmGC. העבודה על קומפילציה מ-Dart ל-Wasm כמעט הסתיימה, והצוות עובד על תמיכה בכלי פיתוח להפצת אפליקציות אינטרנט של Flutter שעברו קומפילציה ל-WebAssembly. אפשר לקרוא על המצב הנוכחי של העבודה במסמכי התיעוד של Flutter. ההדגמה הבאה היא Flutter WasmGC Preview.
מידע נוסף על WasmGC
בפוסט הזה בבלוג נגענו רק בקצה הקרחון וסיפקנו בעיקר סקירה כללית ברמה גבוהה של WasmGC. מידע נוסף על התכונה זמין בקישורים הבאים:
- דרך חדשה להעביר ביעילות שפות תכנות עם איסוף אשפה אל WebAssembly
- סקירה כללית על WasmGC
- WasmGC MVP
- WasmGC post-MVP
תודות
המאמר הזה נבדק על ידי מתיאס לידטקה, אדם קליין, ג'ושוע בל, אלון זקאי, יעקב קומרו, קלמנס באקס, עמנואל ציגלר ורייצ'ל אנדרו.