יש שני סוגים של שפות תכנות: שפות תכנות עם איסוף אשפה ושפות תכנות שדורשות ניהול ידני של זיכרון. דוגמאות לקודים מהסוג הראשון, בין היתר, הן Kotlin, PHP או Java. דוגמאות לקודים כאלה הן C, C++ או Rust. ככלל, בשפות תכנות ברמה גבוהה יותר יש סיכוי גבוה יותר לכך שאיסוף אשפה יהיה תכונה סטנדרטית. בפוסט הזה נסביר על שפות תכנות כאלה עם איסוף אשפה, ונראה איך אפשר לקמפל אותן ל-WebAssembly (Wasm). אבל איך מתחילים לאסוף אשפה (שנקראת בדרך כלל GC)?
תמיכה בדפדפנים
איסוף אשפה
במילים פשוטות, הרעיון של איסוף אשפה הוא ניסיון לנצל מחדש זיכרון שהוקצה על ידי התוכנית, אבל כבר אין אליו הפניה. זיכרון כזה נקרא ''. יש הרבה שיטות להטמעת איסוף אשפה. אחת מהן היא ספירת קובצי עזר, כאשר המטרה היא לספור את מספר ההפניות לאובייקטים בזיכרון. כשאין יותר הפניות לאובייקט, אפשר לסמן אותו כאובייקט שלא בשימוש, וכך הוא מוכן לאיסוף אשפה. מנקה האשפה של 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. קוד האיסוף של האשפה ב-PHP נמצא בעיקר בקובץ zend_gc.c
. רוב המפתחים מתקינים את PHP דרך מנהל החבילות של מערכת ההפעלה שלהם. עם זאת, מפתחים יכולים גם לבנות PHP מקוד המקור. לדוגמה, בסביבת Linux, השלבים ./buildconf && ./configure && make
יוצרים את PHP לסביבת זמן הריצה של Linux. אבל המשמעות היא גם שאפשר לקמפל את סביבת זמן הריצה של PHP לסביבות זמן ריצה אחרות, כמו Wasm.
שיטות מסורתיות לניוד שפות אל סביבת זמן הריצה של Wasm
בנפרד מהפלטפורמה שבה מריצים PHP, הסקריפטים של PHP עוברים לאותו קוד בייטקוד ופועלים על ידי Zend Engine. Zend Engine הוא מהדר וסביבת זמן הריצה של שפת כתיבת הסקריפטים PHP. הוא מורכב מ-Zend Virtual Machine (VM), שמורכב מ-Zend Compiler ומ-Zend Executor. שפות כמו PHP שמוטמעות בשפות אחרות ברמה גבוהה כמו C בדרך כלל כוללות אופטימיזציות שמטרגטות ארכיטקטורות ספציפיות, כמו Intel או ARM, ומחייבות קצה עורפי שונה בכל ארכיטקטורה. בהקשר הזה, Wasm מייצג ארכיטקטורה חדשה. אם למכונה הווירטואלית יש קוד ספציפי לארכיטקטורה, כמו 'בדיוק בזמן' (JIT) או הידור מראש (AOT), המפתח מטמיע גם קצה עורפי עבור JIT/AOT לארכיטקטורה החדשה. הגישה הזו הגיונית, כי לעיתים קרובות אפשר פשוט ליצור מחדש את החלק העיקרי של ה-codebase לכל ארכיטקטורה חדשה.
מכיוון ש-Wasm הוא ברמה נמוכה, טבעי לנסות אותה גישה גם בו: קומפילציה מחדש של קוד ה-VM הראשי עם המנתח, תמיכת הספרייה, איסוף האשפה והאופטימיזטור שלו ל-Wasm, והטמעת קצה עורפי של JIT או AOT ל-Wasm לפי הצורך. אפשר לעשות זאת מאז השקת Wasm MVP, ובמקרים רבים זה עובד טוב בפועל. למעשה, PHP שהורכב ל-Wasm הוא הגורם שמפעיל את WordPress Playground. מידע נוסף על הפרויקט זמין במאמר יצירת חוויות WordPress בדפדפן באמצעות WordPress Playground ו-WebAssembly.
עם זאת, PHP Wasm פועל בדפדפן בהקשר של שפת המארח JavaScript. ב-Chrome, JavaScript ו-Wasm פועלים ב-V8, מנוע JavaScript של Google בקוד פתוח שמטמיע את ECMAScript כפי שמפורט ב-ECMA-262. בנוסף, ב-V8 כבר יש אוסף אשפה. כלומר, מפתחים שמשתמשים, לדוגמה, ב-PHP שעבר הידור ל-Wasm, בסופו של דבר שולחים לדפדפן הטמעה של מנקה האשפה של השפה שהועברה (PHP), שכבר יש לו מנקה אשפה. זה בזבוז זמן, כפי שזה נשמע. כאן נכנסים לתמונה WasmGC.
בעיה נוספת בגישה הישנה, שבה מודול Wasm יוצר את ה-GC שלו מעל הזיכרון הליניארי של Wasm, היא שאין אינטראקציה בין ה-GC של Wasm לבין ה-GC שנוצר מעל השפה שעבר תהליך הידור ל-Wasm. כתוצאה מכך, יש בעיות כמו דליפות זיכרון ונסיונות איסוף לא יעילים. כדי למנוע בעיות כאלה, אפשר לתת למודולים של Wasm לעשות שימוש חוזר ב-GC המובנה הקיים.
העברת שפות תכנות לסביבות זמן ריצה חדשות באמצעות WasmGC
WasmGC היא הצעה של קבוצת הקהילה של WebAssembly. ההטמעה הנוכחית של Wasm MVP יכולה לטפל רק במספרים, כלומר במספרים שלמים וצפים, בזיכרון ליניארי, וכשנשלחת הצעה לסוגי ההפניות, Wasm יכולה גם לשמור קובצי עזר חיצוניים. WasmGC מוסיף עכשיו סוגי אשכול של מבנה נתונים ושל מערך, כלומר תמיכה בהקצאת זיכרון לא לינארית. לכל אובייקט WasmGC יש סוג ומבנה קבועים, כך שמכונות וירטואליות יכולות ליצור בקלות קוד יעיל לגישה לשדות שלהן, בלי הסכנה של ביטול אופטימיזציה שקיימת בשפות דינמיות כמו JavaScript. ההצעה הזו מוסיפה ל-WebAssembly תמיכה יעילה בשפות מנוהלות ברמה גבוהה, באמצעות סוגי אשכול של מבנים ושל מערכי נתונים שמאפשרים למהדרי שפות שמטרגטים את Wasm להתמזג עם מנהל האשפה במכונה הווירטואלית המארחת. במילים פשוטות, המשמעות היא שבעזרת WasmGC, כשממירים שפת תכנות ל-Wasm, לא צריך יותר לכלול את מנקה האשפה של שפת התכנות בפורט, אלא אפשר להשתמש במנקה האשפה הקיים.
כדי לוודא את ההשפעה של השיפור הזה בעולם האמיתי, צוות Wasm של Chrome יצר גרסאות של מדד הביצועים של Fannkuch (שמקצה מבני נתונים תוך כדי עבודה) מ-C, מ-Rust ומ-Java. קובצי ה-binary של C ו-Rust יכולים לנוע בין 6.1K ל-9.6K, בהתאם לדגלים השונים של המהדר, בעוד שגרסת Java קטנה בהרבה – רק 2.3K! C ו-Rust אינם כוללים אספן אשפה, אבל הם עדיין מאגדים malloc/free
לניהול זיכרון, והסיבה ש-Java קטנה יותר כאן היא כי אין צורך לאגד קוד ניהול זיכרון כלל. זו רק דוגמה ספציפית אחת, אבל היא מראה שיש פוטנציאל לקובצי בינאריים של WasmGC להיות קטנים מאוד, וזאת עוד לפני שנעשה עבודה משמעותית לאופטימיזציה של הגודל.
הצגת שפת תכנות שהועברה ל-WasmGC בפעולה
קוטלין וואאם
אחת משפות התכנות הראשונות שהועברו ל-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 Multiplatform מתחיל להיראות הגיוני יותר, כי הוא מאפשר למפתחים להמשיך לפתח את ממשק המשתמש שכבר יצרו לאפליקציות שלהם ל-Android ב-Kotlin. אתם מוזמנים להתנסות בחקירה מוקדמת של התהליך באמצעות ההדגמה של Kotlin/Wasm image Viewer ועיון בקוד המקור שלו, גם באדיבות צוות Kotlin.
קליעה למטרה ורופפים
גם הצוותים של Dart ו-Flutter ב-Google מתכוננים לתמיכה ב-WasmGC. עבודת האיסוף של Drt-to-Wasm כמעט הושלמה, והצוות עובד על תמיכה בכלים כדי לספק אפליקציות אינטרנט של Flutter שנאספו ל-WebAssembly. במסמכי התיעוד של Flutter אפשר לקרוא על הסטטוס הנוכחי של העבודה. הדגמה הבאה היא התצוגה המקדימה של Flutter WasmGC.
מידע נוסף על WasmGC
הפוסט הזה הוא רק קצה המזלג, והוא מספק בעיקר סקירה כללית ברמה גבוהה של WasmGC. מידע נוסף על התכונה זמין בקישורים הבאים:
- דרך חדשה להעביר באופן יעיל את שפות תכנות שנאספו באמצעות אשפה ל-WebAssembly
- סקירה כללית על WasmGC
- MVP של WasmGC
- WasmGC אחרי MVP
תודות
הבדיקה של המאמר בוצעה על ידי Matthias Liedtke, Adam Klein, Joshua Bell, Alon Zakai, Jakob Kummerow, Clemens Backes, Emanuel Ziegler ו-Rachel Andrew.