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

Colt McAnlis
Colt McAnlis

מבוא

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

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

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

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

המשמעות של שיניים

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

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

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

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

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

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

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

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

התהליך הזה יזיז את תרשים הזיכרון מהנקודה הבאה :

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

עד כאן:

JavaScript של זיכרון סטטי

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

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

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

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

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

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

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

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

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, רנאטו מנגיני, יש כמה חסרונות.

סיכום

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

קוד מקור

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

קובצי עזר