זיכרון סטטי ב-JavaScript עם מאגרי אובייקטים

Colt McAnlis
Colt McAnlis

מבוא

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

תמונת מצב מציר הזמן של הזיכרונות

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

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

מה המשמעות של שיני המסור

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

איסוף אשפה ועלויות ביצועים

מודל הזיכרון של JavaScript מבוסס על טכנולוגיה שנקראת Garbage Collector. בשפות רבות, המתכנת אחראי ישירות להקצאה ולשחרור של זיכרון מערימת הזיכרון של המערכת. לעומת זאת, מערכת Garbage Collector מנהלת את המשימה הזו בשביל המתכנת. כלומר, האובייקטים לא משוחררים ישירות מהזיכרון כשהמתכנת מבטל את ההפניה אליהם, אלא במועד מאוחר יותר, כשההיוריסטיקה של ה-GC מחליטה שזה יהיה מועיל. תהליך קבלת ההחלטות הזה דורש מה-GC לבצע ניתוח סטטיסטי של אובייקטים פעילים ולא פעילים, וזה לוקח זמן.

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

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

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

הפחתת העמסת הזיכרון והפחתת המיסים על איסוף אשפה

כמו שצוין, פולס של GC יתרחש ברגע שקבוצה של היוריסטיקות תקבע שיש מספיק אובייקטים לא פעילים, כך שפולס יועיל. לכן, הדרך לקצר את משך הזמן שבו פועל מנגנון איסוף הזבל באפליקציה היא לצמצם ככל האפשר את המקרים של יצירה ושחרור של אובייקטים מיותרים. התהליך הזה של יצירה/שחרור של אובייקטים בתדירות גבוהה נקרא "העמסת זיכרון". אם תצליחו לצמצם את העמסת הזיכרון במהלך כל משך החיים של האפליקציה, תוכלו גם לצמצם את משך הזמן שבו מתבצע GC במהלך ההרצה. כלומר, צריך להסיר או להפחית את מספר האובייקטים שנוצרים ונהרסים, ולמעשה להפסיק להקצות זיכרון.

התהליך הזה יעביר את תרשים הזיכרון שלכם מהמצב הזה :

תמונת מצב מציר הזמן של הזיכרונות

לזה:

‫JavaScript עם זיכרון סטטי

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

מעבר ל-JavaScript עם זיכרון סטטי

Static Memory JavaScript היא טכניקה שכוללת הקצאה מראש, בתחילת האפליקציה, של כל הזיכרון שיידרש במהלך כל משך החיים שלה, וניהול הזיכרון הזה במהלך ההפעלה כשאובייקטים כבר לא נדרשים. כדי להשיג את היעד הזה, אפשר לבצע כמה שלבים פשוטים:

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

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

מאגר אובייקטים

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

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

var newEntity = gEntityObjectPool.allocate();
newEntity.pos = {x: 215, y: 88};

//..... do some stuff with the object that we need to do

gEntityObjectPool.free(newEntity); //free the object when we're done
newEntity = null; //free this object reference

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

הקצאה מראש של אובייקטים

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

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

function init() {
  //preallocate all our pools. 
  //Note that we keep each pool homogeneous wrt object types
  gEntityObjectPool.preAllocate(256);
  gDomObjectPool.preAllocate(888);
}

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

זה לא פתרון קסם

יש סיווג שלם של אפליקציות שבהן דפוסי צמיחה סטטיים של זיכרון יכולים להועיל. עם זאת, כמו שציין רנאטו מנג'יני, חבר בצוות Chrome DevRel, יש כמה חסרונות.

סיכום

אחת מהסיבות לכך ש-JavaScript היא שפה אידיאלית לאינטרנט היא שקל להתחיל להשתמש בה, והיא מהירה ומהנה. הסיבה העיקרית לכך היא שאין הרבה מגבלות תחביר, והיא מטפלת בבעיות בזיכרון בשבילכם. אתם יכולים להתמקד בכתיבת הקוד ולתת לו לטפל בעבודה השחורה. עם זאת, באפליקציות אינטרנט עם ביצועים גבוהים, כמו משחקי HTML5, איסוף הזבל יכול לפגוע בקצב הפריימים, שחשוב מאוד לחוויית המשתמש. בעזרת שימוש זהיר בכלים ושימוש במאגרי אובייקטים, אתם יכולים להפחית את העומס על קצב הפריימים, ולנצל את הזמן הזה לדברים מדהימים אחרים.

קוד מקור

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

קובצי עזר