צוות Android Runtime (ART) קיצר את משך הזמן לקימפול ב-18% בלי לפגוע בקוד המהודר או בנסיגות בזיכרון השיא. השיפור הזה הוא חלק מהיוזמה שלנו לשנת 2025 לשיפור משך הזמן לקימפול בלי להתפשר על שימוש בזיכרון או על איכות הקוד המהודר.
אופטימיזציה של מהירות ההידור היא קריטית ל-ART. לדוגמה, כשמבצעים הידור בזמן ריצה (JIT), היא משפיעה ישירות על היעילות של האפליקציות ועל הביצועים הכוללים של המכשיר. הידור מהיר יותר מקצר את הזמן עד שהאופטימיזציות מתחילות לפעול, וכך משפר את חוויית המשתמש והופך אותה לחלקה יותר. בנוסף, גם בהידור בזמן ריצה (JIT) וגם בהידור מראש (AOT), שיפורים במהירות ההידור מובילים לצריכת משאבים מופחתת במהלך תהליך ההידור, וכך מאריכים את חיי הסוללה ומשפרים את התכונות התרמיות של המכשיר, במיוחד במכשירים ברמת כניסה.
חלק מהשיפורים האלה במהירות ההידור הושקו בגרסת Android מיוני 2025, והשאר יהיו זמינים בגרסת Android של סוף השנה. בנוסף, כל משתמשי Android בגרסה 12 ומעלה יכולים לקבל את השיפורים האלה באמצעות עדכונים ראשיים.
אופטימיזציה של מהדר האופטימיזציה
אופטימיזציה של קומפיילר היא תמיד משחק של פשרות. אי אפשר לקבל מהירות בחינם, צריך לוותר על משהו. הצבנו לעצמנו מטרה ברורה ומאתגרת: להאיץ את המהדר, אבל בלי לגרום לרגרסיות בזיכרון, וחשוב מכך, בלי לפגוע באיכות הקוד שהוא מייצר. אם הקומפיילר מהיר יותר אבל האפליקציות רצות לאט יותר, נכשלנו.
המשאב היחיד שהיינו מוכנים להשקיע היה זמן הפיתוח שלנו, כדי לחקור לעומק ולמצוא פתרונות חכמים שעומדים בקריטריונים המחמירים האלה. בואו נבחן מקרוב איך אנחנו פועלים כדי למצוא תחומים לשיפור, ואיך אנחנו מוצאים את הפתרונות הנכונים לבעיות השונות.
איתור אופטימיזציות אפשריות שכדאי לבצע
כדי להתחיל לבצע אופטימיזציה של מדד, צריך קודם למדוד אותו. אחרת, לא תוכלו לדעת אם שיפרתם אותו או לא. למזלנו, משך הזמן לקימפול די עקבי כל עוד נוקטים אמצעי זהירות מסוימים, כמו שימוש באותו מכשיר שבו השתמשתם למדידה לפני ואחרי שינוי, ולוודא שלא מתבצעת במכשיר ויסות נתונים עקב התחממות יתר. בנוסף, יש לנו גם מדידות דטרמיניסטיות כמו נתונים סטטיסטיים של קומפיילר, שעוזרות לנו להבין מה קורה מתחת לפני השטח.
המשאב שהקרבנו לטובת השיפורים האלה היה זמן הפיתוח שלנו, ולכן רצינו לבצע איטרציות מהר ככל האפשר. המשמעות היא שלקחנו כמה אפליקציות מייצגות (שילוב של אפליקציות מהדומיין הנוכחי, אפליקציות של צד שלישי ומערכת ההפעלה Android עצמה) כדי ליצור אב טיפוס של פתרונות. בהמשך, אימתנו שההטמעה הסופית הייתה שווה את המאמץ באמצעות בדיקות ידניות ואוטומטיות נרחבות.
עם קובצי ה-APK שנבחרו בקפידה, הפעלנו קומפילציה ידנית באופן מקומי, קיבלנו פרופיל של הקומפילציה והשתמשנו ב-pprof כדי להמחיש איפה אנחנו משקיעים את הזמן שלנו.
דוגמה לתרשים להבות של פרופיל ב-pprof
הכלי pprof הוא כלי רב עוצמה שמאפשר לנו לפלח, לסנן ולמיין את הנתונים כדי לראות, לדוגמה, אילו שלבים או שיטות של קומפילציה צורכים הכי הרבה זמן. לא נפרט על pprof עצמו, רק נציין שאם הסרגל גדול יותר, המשמעות היא שהקומפילציה ארכה יותר זמן.
אחת מהתצוגות האלה היא 'מלמטה למעלה', שבה אפשר לראות אילו שיטות צורכות הכי הרבה זמן. בתמונה שלמטה אפשר לראות שיטה בשם Kill, שמהווה יותר מ-1% ממשך הזמן לקימפול. בהמשך הפוסט בבלוג נדון גם בכמה מהשיטות המובילות האחרות.
תצוגה של פרופיל מלמטה למעלה
במהדר האופטימיזציה שלנו יש שלב שנקרא Global Value Numbering (מספור ערכים גלובלי, GVN). אתם לא צריכים לדאוג לגבי מה שהיא עושה באופן כללי, אבל החלק הרלוונטי הוא לדעת שיש לה שיטה שנקראת Kill, שבאמצעותה היא תמחק חלק מהצמתים בהתאם למסנן. הפעולה הזו אורכת זמן רב כי היא מחייבת מעבר על כל הצמתים ובדיקה של כל אחד מהם בנפרד. שמנו לב שיש מקרים שבהם אנחנו יודעים מראש שהבדיקה תחזיר ערך שקר, לא משנה אילו צמתים פעילים לנו באותו רגע. במקרים כאלה, אנחנו יכולים לדלג על איטרציות לגמרי, ולהקטין את שיעור השגיאות מ-1.023% לכ-0.3%, ולשפר את זמן הריצה של GVN בכ-15%.
הטמעה של אופטימיזציות משתלמות
הסברנו איך למדוד ואיך לזהות איפה הזמן מנוצל, אבל זו רק ההתחלה. השלב הבא הוא אופטימיזציה של הזמן שמוקדש לקומפילציה.
בדרך כלל, במקרה כמו `Kill` שצוין למעלה, נבדוק איך אנחנו חוזרים על הפעולות בצמתים ואיך אפשר לעשות את זה מהר יותר. למשל, על ידי ביצוע פעולות במקביל או שיפור האלגוריתם עצמו. למעשה, זה מה שניסינו בהתחלה, ורק כשלא מצאנו מה לעשות, הייתה לנו תובנה שהפתרון הוא (במקרים מסוימים) לא לחזור על הפעולה בכלל! כשמבצעים אופטימיזציות כאלה, קל להתמקד בפרטים הקטנים ולפספס את התמונה הגדולה.
במקרים אחרים, השתמשנו בכמה טכניקות שונות, כולל:
- שימוש בהיוריסטיקה כדי להחליט אם אופטימיזציה מסוימת לא תניב תוצאות משתלמות ולכן אפשר לדלג עליה
- שימוש במבני נתונים נוספים כדי לשמור במטמון נתונים מחושבים
- שינוי מבני הנתונים הנוכחיים כדי לשפר את המהירות
- חישוב התוצאות מתבצע רק כשצריך כדי למנוע מחזורים במקרים מסוימים
- להשתמש בהפשטה הנכונה – תכונות מיותרות עלולות להאט את הקוד
- כדי להימנע ממצב שבו צריך לחפש סמן שנמצא בשימוש לעתים קרובות בין הרבה טעינות
איך אפשר לדעת אם כדאי לבצע את האופטימיזציות?
החלק המעניין הוא שלא צריך. אחרי שמזהים שאזור מסוים צורך הרבה זמן קומפילציה, ואחרי שמקדישים זמן פיתוח כדי לנסות לשפר אותו, לפעמים פשוט אי אפשר למצוא פתרון. יכול להיות שאין מה לעשות, שייקח יותר מדי זמן ליישם פתרון, שפתרון מסוים יגרום לירידה משמעותית במדד אחר, שיגדיל את המורכבות של בסיס הקוד וכו'. לכל אופטימיזציה מוצלחת שמופיעה בפוסט הזה בבלוג, יש אינספור אופטימיזציות אחרות שלא צלחו.
אם אתם במצב דומה, נסו להעריך עד כמה תשפרו את המדד על ידי ביצוע כמה שפחות עבודה. כלומר, בסדר הבא:
- הערכה באמצעות מדדים שכבר אספתם, או סתם תחושת בטן
- הערכה באמצעות אב-טיפוס מהיר ופשוט
- הטמעת פתרון.
אל תשכחו להעריך את החסרונות של הפתרון. לדוגמה, אם אתם מתכוונים להסתמך על מבני נתונים נוספים, כמה זיכרון אתם מוכנים להקצות?
בחינה מעמיקה
בלי הקדמות מיותרות, נבחן כמה מהשינויים שהטמענו.
יישמנו שינוי כדי לבצע אופטימיזציה של שיטה שנקראת FindReferenceInfoOf. השיטה הזו ביצעה חיפוש לינארי של וקטור כדי למצוא רשומה. עדכנו את מבנה הנתונים הזה כך שהוא יאונדקס לפי מזהה ההוראה, כדי ש-FindReferenceInfoOf יהיה O(1) במקום O(n). בנוסף, הקצנו מראש את הווקטור כדי למנוע שינוי גודל. הגדלנו מעט את הזיכרון כי היינו צריכים להוסיף שדה נוסף שסופר כמה רשומות הוספנו לווקטור, אבל זה היה שינוי קטן כי השימוש בזיכרון לא גדל. השינוי הזה קיצר את שלב LoadStoreAnalysis ב-34-66%, וכתוצאה מכך קיבלנו שיפור של כ-0.5-1.8% בזמן ההידור.
יש לנו הטמעה מותאמת אישית של HashSet שבה אנחנו משתמשים בכמה מקומות. יצירת מבנה הנתונים הזה ארכה זמן רב, וגילינו למה. לפני שנים רבות, מבנה הנתונים הזה שימש רק בכמה מקומות שהשתמשו ב-HashSet גדולים מאוד, והוא עבר שינויים כדי להיות מותאם לשימוש הזה. אבל כיום משתמשים בו בכיוון ההפוך, עם כמה ערכים בלבד ועם משך חיים קצר. המשמעות היא שבזבזנו מחזורים ביצירת HashSet גדול, אבל השתמשנו בו רק לכמה רשומות לפני שהשלכנו אותו. בעקבות השינוי הזה, שיפרנו את זמן ההידור בכ-1.3-2%. בנוסף, השימוש בזיכרון ירד בכ-0.5-1% כי לא השתמשנו במבני נתונים גדולים כמו קודם.
שיפרנו את משך הזמן לקימפול בכ-0.5% עד 1% על ידי העברת מבני נתונים באמצעות הפניה אל פונקציית ה-lambda, כדי להימנע מהעתקה שלהם. זה היה משהו שלא שמנו לב אליו בבדיקה המקורית, והוא היה בבסיס הקוד שלנו במשך שנים. רק אחרי שבדקנו את הפרופילים ב-pprof, שמנו לב שהשיטות האלה יוצרות והורסות הרבה מבני נתונים, ולכן חקרנו אותן וביצענו אופטימיזציה שלהן.
קיצרנו את השלב שבו נכתב הפלט המהודר על ידי שמירת ערכים מחושבים במטמון, מה שהוביל לשיפור של כ-1.3% עד 2.8% בזמן ההידור הכולל. לצערנו, העומס של ניהול החשבונות היה גדול מדי, והבדיקות האוטומטיות שלנו התריעו על רגרסיה בזיכרון. בהמשך, בדקנו שוב את אותו קוד והטמענו גרסה חדשה שלא רק טיפלה בנסיגה בזיכרון, אלא גם שיפרה את משך הזמן לקימפול בעוד כ-0.5-1.8%! בשינוי השני הזה נאלצנו לשנות את המבנה של השלב הזה ולחשוב מחדש איך הוא צריך לפעול, כדי להיפטר מאחד משני מבני הנתונים.
במהדר האופטימיזציה שלנו יש שלב שבו מתבצעת החלפה של קריאות לפונקציות בקוד שלהן, כדי לשפר את הביצועים. כדי לבחור אילו מתודות להחליף בקוד שלהן, אנחנו משתמשים בהיוריסטיקה לפני שאנחנו מבצעים חישובים, ובבדיקות סופיות אחרי שאנחנו מבצעים עבודה אבל לפני שאנחנו מסיימים את ההחלפה. אם אחת מהבדיקות האלה מזהה שההחלפה לא משתלמת (לדוגמה, אם יתווספו יותר מדי הוראות חדשות), אנחנו לא מחליפים את הקריאה למתודה בקוד שלה.
העברנו שתי בדיקות מהקטגוריה 'בדיקות סופיות' לקטגוריה 'היוריסטיקה' כדי להעריך אם ההטמעה תצליח או לא לפני שנבצע חישובים יקרים מבחינת זמן. מכיוון שמדובר באומדן, הוא לא מושלם, אבל וידאנו שהיוריסטיקות החדשות שלנו מכסות 99.9% ממה שהיה מוטבע קודם, בלי להשפיע על הביצועים. אחת מההיוריסטיקות החדשות הייתה לגבי הרישומים הנדרשים של DEX (שיפור של כ-0.2% עד 1.3%), והשנייה לגבי מספר ההוראות (שיפור של כ-2%).
יש לנו הטמעה מותאמת אישית של BitVector שבה אנחנו משתמשים בכמה מקומות. החלפנו את המחלקה BitVector שניתן לשנות את הגודל שלה במחלקה BitVectorView פשוטה יותר עבור וקטורים בינאריים בגודל קבוע. הפעולה הזו מבטלת חלק מההפניות העקיפות ובדיקות הטווח בזמן הריצה, ומאיצה את יצירת האובייקטים של וקטור הביטים.
בנוסף, המחלקה BitVectorView עברה שימוש בתבניות (templatized) בסוג האחסון הבסיסי (במקום להשתמש תמיד ב-uint32_t כמו ב-BitVector הישן). ההגדרה הזו מאפשרת לבצע פעולות מסוימות, כמו Union(), ולעבד פי שניים יותר ביטים ביחד בפלטפורמות 64-bit. המדגמים של הפונקציות המושפעות קוצצו ביותר מ-1% בסך הכול במהלך קומפילציה של Android OS. השינוי הזה בוצע בכמה שינויים [1, 2, 3, 4, 5, 6]
אם היינו מדברים בפירוט על כל האופטימיזציות, היינו נשארים כאן כל היום. אם תרצה לבצע אופטימיזציות נוספות, כדאי לעיין בשינויים אחרים שהטמענו:
- הוספת ניהול חשבונות כדי לשפר את זמני הקומפילציה בכ-0.6-1.6%.
- מחשבים נתונים בצורה עצלה כדי להימנע ממחזורים, אם אפשר.
- שינוי מבנה הקוד כדי לדלג על עבודת חישוב מראש אם לא נעשה בה שימוש.
- כדאי להימנע משרשרות טעינה תלויות מסוימות אם אפשר להשיג את המקצה בקלות ממקומות אחרים.
- מקרה נוסף של הוספת בדיקה כדי למנוע עבודה מיותרת.
- מומלץ להימנע מפיצולים תכופים בסוג הרישום (ליבה/FP) במקצה הרישומים.
- מוודאים שחלק מהמערכים מאותחלים בזמן ההידור. אל תסתמכו על clang כדי לעשות זאת.
- ניקוי של כמה לולאות. כדאי להשתמש בלולאות טווח שאפשר לבצע בהן אופטימיזציה טובה יותר באמצעות clang, כי אין צורך לטעון מחדש את המצביעים הפנימיים של המאגר בגלל תופעות לוואי של הלולאה. מומלץ להימנע מקריאה לפונקציה הווירטואלית `HInstruction::GetInputRecords()` בלולאה באמצעות `InputAt(.)` מוטבעת לכל קלט.
- הימנעו מפונקציות Accept() עבור דפוס המבקר על ידי ניצול אופטימיזציה של קומפיילר.
סיכום
ההתמקדות שלנו בשיפור מהירות ההידור של ART הניבה שיפורים משמעותיים, שהופכים את Android לזורם ויעיל יותר, וגם תורמים לשיפור חיי הסוללה והטמפרטורה של המכשיר. באמצעות זיהוי קפדני של אופטימיזציות והטמעה שלהן, הראינו שאפשר להשיג שיפורים משמעותיים בזמן ההידור בלי לפגוע בשימוש בזיכרון או באיכות הקוד.
התהליך שלנו כלל יצירת פרופילים באמצעות כלים כמו pprof, נכונות לחזור על פעולות, ולפעמים אפילו נטישה של דרכים פחות יעילות. המאמצים המשותפים של צוות ART לא רק צמצמו את זמן ההידור באחוז משמעותי, אלא גם הניחו את הבסיס להתקדמות עתידית.
כל השיפורים האלה זמינים בעדכון Android של סוף שנת 2025, וב-Android מגרסה 12 ומעלה באמצעות עדכונים ראשיים. אנחנו מקווים שהסקירה המעמיקה הזו של תהליך האופטימיזציה שלנו תספק לכם תובנות חשובות לגבי המורכבות והיתרונות של הנדסת קומפיילרים.
להמשך הקריאה
-
חדשות על מוצרים
בכל שנה, ב-Google I/O מוצגים משאבים והודעות חדשים לגבי מערכות אקולוגיות ומוצרים, כולל פיתוח ל-Android. הפיתוח עובר לכיוון של AI וכלים מבוססי-סוכנים, ולכן הרחבנו את ההצעות שלנו כדי לתמוך בכם בצורה טובה יותר, לא משנה איך תבחרו לפתח ל-Android.
Simona Milanovic • משך הקריאה: 2 דקות
-
חדשות על מוצרים
ב-Google I/O 2026, הצגנו איך החידושים האחרונים במערכת האקולוגית של Android יכולים לעזור לכם לשפר את איכות האפליקציה ולמקסם את יעילות הפיתוח.
Ataul Munim • משך הקריאה: 3 דקות
-
חדשות על מוצרים
באירוע Google I/O 2026, הצגנו את השינוי ב-Android ממערכת הפעלה למערכת חכמה. הדגמנו גם איך אפשר ליצור חוויות חכמות באופן מקורי באמצעות המערכת, ולשלב את היכולות של ה-AI של Google באפליקציות שלכם.
Jingyu Shi • משך הקריאה: 2 דקות
כדאי תמיד להיות בעניינים
רוצים לקבל טיפים עדכניים לפיתוח Android ישירות לאימייל כל שבוע?