אופטימיזציה מבוססת-פרופיל (PGO) היא שיטת אופטימיזציה ידועה של המהדר. ב-PGO, המהדר משתמש בפרופילים של זמן ריצה מהפעלות של תוכנית כדי לקבל החלטות אופטימליות לגבי הטמעה בקוד (inline) וסידור הקוד. כתוצאה מכך, הביצועים משתפרים והקוד קטן יותר.
אפשר לפרוס את PGO באפליקציה או בספרייה באופן הבא: 1. מזהים עומס עבודה מייצג. 2. איסוף פרופילים. 3. שימוש בפרופילים בגרסת ה-build של הגרסה.
שלב 1: זיהוי עומס עבודה מייצג
קודם כול, מזהים מדד ביצועים או עומס עבודה מייצגים לאפליקציה. זהו שלב קריטי, כי הפרופילים שנאספו מעומס העבודה מזהים את האזורים החמים והקרים בקוד. כשמשתמשים בפרופילים, המהדר מבצע אופטימיזציה אגרסיבית והטמעה בקוד (inline) באזורים החמים. המהדר עשוי גם להקטין את גודל הקוד של אזורים קרים תוך הפחתת הביצועים.
זיהוי עומס עבודה טוב גם עוזר לעקוב אחרי הביצועים באופן כללי.
שלב 2: איסוף פרופילים
איסוף הפרופילים כולל שלושה שלבים: - בניית קוד נייטיב עם אינסטרומנטציה, - הפעלת האפליקציה עם האינסטרומנטציה במכשיר ויצירת פרופילים ו- מיזוג או עיבוד לאחר מכן של הפרופילים במארח.
יצירת build עם כלי למדידת ביצועים
כדי לאסוף את הפרופילים, מריצים את עומס העבודה משלב 1 ב-build של האפליקציה עם כלים למדידה. כדי ליצור build עם אינסטרומנטציה, מוסיפים את -fprofile-generate לדגלי המהדר והקישור. צריך לשלוט בדגל הזה באמצעות משתנה build נפרד, כי הדגל לא נדרש במהלך build שמוגדר כברירת מחדל.
יצירת פרופילים
בשלב הבא, מריצים את האפליקציה עם הכלי למדידת ביצועים במכשיר ויוצרים פרופילים.
הפרופילים נאספים בזיכרון כשמריצים את קובץ הבינארי המתווסף, ונכתבים לקובץ בסיום. עם זאת, פונקציות שרשומות ב-atexit לא נקראות באפליקציה ל-Android – האפליקציה פשוט נמחקת.
האפליקציה או עומס העבודה צריכים לבצע עבודה נוספת כדי להגדיר נתיב לקובץ הפרופיל, ולאחר מכן להפעיל באופן מפורש כתיבת פרופיל.
- כדי להגדיר את הנתיב של קובץ הפרופיל, צריך להפעיל את הפונקציה
__llvm_profile_set_filename(PROFILE_DIR "/default-%m.profraw.%mשימושי כשיש מספר ספריות משותפות.%mמתרחב לחתימה ייחודית של המודול בספרייה הזו, וכתוצאה מכך נוצר פרופיל נפרד לכל ספרייה. כאן מפורטים עוד מפרטים שימושיים של דפוסים.PROFILE_DIRהיא ספרייה שאפשר לכתוב בה מהאפליקציה. אפשר לעיין בדוגמה כדי לזהות את הספרייה הזו בזמן הריצה. - כדי להפעיל באופן מפורש כתיבה של פרופיל, צריך להפעיל את הפונקציה
__llvm_profile_write_file.
extern "C" {
extern int __llvm_profile_set_filename(const char*);
extern int __llvm_profile_write_file(void);
}
#define PROFILE_DIR "<location-writable-from-app>"
void workload() {
// ...
// run workload
// ...
// set path and write profiles after workload execution
__llvm_profile_set_filename(PROFILE_DIR "/default-%m.profraw");
__llvm_profile_write_file();
return;
}
הערה: קל יותר ליצור את קובץ הפרופיל אם עומס העבודה הוא קובץ בינארי עצמאי – פשוט מגדירים את משתנה הסביבה LLVM_PROFILE_FILE לערך %t/default-%m.profraw לפני שמריצים את הקובץ הבינארי.
פרופילים של עיבוד נתונים לאחר פרסום
קובצי הפרופיל הם בפורמט .profraw. קודם צריך לאחזר אותם מהמכשיר באמצעות adb pull. אחרי האחזור, משתמשים בכלי השירות llvm-profdata ב-NDK כדי להמיר מ-.profraw ל-.profdata, ואז ניתן להעביר אותו למהדר.
$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-profdata \
merge --output=pgo_profile.profdata \
<list-of-profraw-files>
כדי למנוע אי-התאמה בין הגרסאות של פורמטים של קובצי הפרופיל, צריך להשתמש ב-llvm-profdata וב-clang מאותה גרסה של NDK.
שלב 3: שימוש בפרופילים ליצירת אפליקציה
תוכלו להשתמש בפרופיל מהשלב הקודם במהלך גרסת ה-build של האפליקציה, על ידי העברת -fprofile-use=<>.profdata למהדר ולמקשר. אפשר להשתמש בפרופילים גם כשהקוד מתפתח – המהדר Clang יכול לסבול אי-התאמה קלה בין המקור לבין הפרופילים.
הערה: באופן כללי, ברוב הספריות הפרופילים זהים בכל הארכיטקטורות. לדוגמה, אפשר להשתמש בפרופילים שנוצרו מ-build של הספרייה ב-arm64 לכל הארכיטקטורות. עם זאת, אם בספרייה יש נתיבים של קוד ספציפיים לארכיטקטורה (arm לעומת x86 או 32 ביט לעומת 64 ביט), צריך להשתמש בפרופילים נפרדים לכל הגדרה כזו.
סיכום של כל המידע
בדף https://github.com/DanAlbert/ndk-samples/tree/pgo/pgo תוכלו לראות הדגמה מקצה לקצה של שימוש ב-PGO מאפליקציה. הדף הזה מכיל פרטים נוספים שלא התייחסנו אליהם במאמר הזה.
- כללי ה-build CMake מראים איך להגדיר משתנה CMake שיוצר קוד מקורי עם אינסטרומנטציה. כשמשתנה ה-build לא מוגדר, מתבצעת אופטימיזציה של קוד נייטיב באמצעות פרופילי PGO שנוצרו בעבר.
- ב-build שמוגדר באינסטרומנטציה, התגים ב-pgodemo.cpp כותבים שהפרופילים הם ביצוע של עומס עבודה.
- מיקום לכתיבה של הפרופילים מתקבל במהלך זמן הריצה ב-MainActivity.kt באמצעות
applicationContext.cacheDir.toString(). - כדי לשלוף פרופילים מהמכשיר בלי לדרוש
adb root, אפשר להשתמש במתכון שלadbכאן.