תאריך הפרסום:
Android 12 (רמת API 31) – Performance Hint API
Android 13 (רמת API 33) – Performance Hint Manager ב-NDK API
(תצוגה מקדימה) Android 15 (DP1) – reportActualWorkDuration()
בעזרת רמזים לביצועי המעבד, משחק יכול להשפיע על התנהגות דינמית של ביצועי המעבד כדי להתאים טוב יותר לצרכים שלו. ברוב המכשירים, מערכת Android מתאימה באופן דינמי את מהירות שעון המעבד ואת סוג הליבה לעומס עבודה על סמך הדרישות הקודמות. אם עומס עבודה משתמש ביותר משאבי CPU, מהירות השעון עולה ועומס העבודה עובר בסופו של דבר לליבה גדולה יותר. אם עומס העבודה משתמש בפחות משאבים, מערכת Android מקטינה את הקצאת המשאבים. בעזרת ADPF, האפליקציה או המשחק יכולים לשלוח אות נוסף לגבי הביצועים ולוחות הזמנים שלהם. כך המערכת יכולה להגביר את המהירות בצורה אגרסיבית יותר (לשפר את הביצועים) ולהפחית את מהירות השעון במהירות כשהעומס מסתיים (לחסוך בצריכת החשמל).
מהירות שעון
כשמכשירי Android משנים באופן דינמי את מהירות השעון של המעבד, התדירות יכולה לשנות את הביצועים של הקוד. חשוב לתכנן קוד שמתייחס למהירויות שעון דינמיות כדי למקסם את הביצועים, לשמור על מצב תרמי בטוח ולהשתמש בחשמל בצורה יעילה. אי אפשר להקצות תדרי CPU ישירות בקוד האפליקציה. כתוצאה מכך, דרך נפוצה שבה אפליקציות מנסות לפעול במהירויות שעון גבוהות יותר של המעבד היא הפעלת לולאה פעילה בשרשור רקע, כדי שעומס העבודה ייראה תובעני יותר. זהו נוהג לא מומלץ, כי הוא מבזב אנרגיה ומגדיל את העומס התרמי על המכשיר כשהאפליקציה לא משתמשת בפועל במשאבים הנוספים. ה-API של המעבד PerformanceHint
נועד לפתור את הבעיה הזו. אם תעדכנו את המערכת לגבי משך העבודה בפועל ומשך העבודה הרצוי, מערכת Android תוכל לקבל סקירה כללית של צורכי המעבד של האפליקציה ולהקצות משאבים בצורה יעילה. כך תשיגו ביצועים אופטימליים ברמת צריכת חשמל יעילה.
סוגים עיקריים
סוגי ליבות המעבד שהמשחק פועל עליהן הם עוד גורם חשוב שמשפיע על הביצועים. במכשירי Android, ליבת המעבד שמוקצית לשרשור משתנה לעיתים קרובות באופן דינמי על סמך התנהגות עומס העבודה האחרונה. הקצאת ליבות CPU מורכבת עוד יותר במערכות על שבב (SoC) עם כמה סוגי ליבות. בחלק מהמכשירים האלה, אפשר להשתמש בליבות הגדולות רק לזמן קצר בלי להיכנס למצב לא יציב מבחינת טמפרטורה.
המשחק לא צריך לנסות להגדיר את הקרבה של ליבת המעבד מהסיבות הבאות:
- סוג הליבה הכי טוב לעומס עבודה משתנה בהתאם לדגם המכשיר.
- היכולת להפעיל ליבות גדולות יותר משתנה בהתאם ל-SoC ולפתרונות התרמיים השונים שכלולים בכל דגם מכשיר.
- ההשפעה הסביבתית על מצב החום יכולה לסבך עוד יותר את הבחירה הבסיסית. לדוגמה, מזג האוויר או כיסוי לטלפון יכולים לשנות את מצב הטמפרטורה של המכשיר.
- אי אפשר להוסיף מכשירים חדשים עם ביצועים ויכולות תרמיות נוספים. כתוצאה מכך, המכשירים מתעלמים לעיתים קרובות מהקצאת ליבות המעבד למשחק.
דוגמה להתנהגות ברירת המחדל של מתזמן ב-Linux

ה-API של PerformanceHint מפשט יותר מאשר את זמני האחזור של DVFS

- אם המשימות צריכות לפעול במעבד ספציפי, ממשק ה-API PerformanceHint יודע איך לקבל את ההחלטה הזו בשמכם.
- לכן, אין צורך להשתמש בהעדפה.
- למכשירים יש טופולוגיות שונות, והמאפיינים של צריכת החשמל והחום משתנים מדי, ולכן הם לא מוצגים למפתחי האפליקציות.
- אי אפשר להניח הנחות לגבי המערכת הבסיסית שבה אתם משתמשים.
הפתרון
ה-ADPF מספק את המחלקה PerformanceHintManager
כדי שמשחקים יוכלו לשלוח רמזים ל-Android לגבי הביצועים של מהירות השעון של המעבד וסוג הליבה. לאחר מכן, מערכת ההפעלה יכולה להחליט איך הכי טוב להשתמש ברמזים על סמך ה-SoC והפתרון התרמי של המכשיר. אם האפליקציה משתמשת ב-API הזה יחד עם מעקב אחרי מצב הטמפרטורה, היא יכולה לספק רמזים מושכלים יותר למערכת ההפעלה במקום להשתמש בלולאות עמוסות ובטכניקות קידוד אחרות שעלולות לגרום להגבלת קצב העברת הנתונים.
כך משחק משתמש בהצעות לשיפור הביצועים:
- יוצרים סשנים של רמזים לשרשורים מרכזיים שמתנהגים באופן דומה. לדוגמה:
- שרשור העיבוד והתלויות שלו מקבלים סשן אחד
- ב-Cocos, ה-thread הראשי של המנוע וה-thread של הרינדור מקבלים סשן אחד
- ב-Unity, מטמיעים את הפלאגין Adaptive Performance Android Provider
- ב-Unreal, משלבים את הפלאגין Unreal Adaptive Performance ומשתמשים באפשרויות ההתאמה כדי לתמוך בכמה רמות איכות
- שרשורי IO מקבלים סשן נוסף
- לשרשורי אודיו יש סשן שלישי
- שרשור העיבוד והתלויות שלו מקבלים סשן אחד
- המשחק צריך לעשות את זה מוקדם, לפחות 2ms ועדיף יותר מ-4ms לפני שנדרשים למערכת יותר משאבים.
- בכל סשן של רמזים, צריך לחזות את משך הזמן שיידרש להרצת כל סשן. המשך האופייני שווה למרווח בין פריימים, אבל האפליקציה יכולה להשתמש במרווח קצר יותר אם עומס העבודה לא משתנה באופן משמעותי בין הפריימים.
כך עוברים מתיאוריה למעשים:
הפעלת PerformanceHintManager ויצירת hintSession
מקבלים את המנהל באמצעות שירות המערכת ויוצרים סשן של רמזים לשרשור או לקבוצת השרשורים שעובדים על אותה עומס עבודה.
C++
int32_t tids[1];
tids[0] = gettid();
int64_t target_fps_nanos = getFpsNanos();
APerformanceHintManager* hint_manager = APerformanceHint_getManager();
APerformanceHintSession* hint_session =
APerformanceHint_createSession(hint_manager, tids, 1, target_fps_nanos);
Java
int[] tids = {
android.os.Process.myTid()
};
long targetFpsNanos = getFpsNanos();
PerformanceHintManager performanceHintManager =
(PerformanceHintManager) this.getSystemService(Context.PERFORMANCE_HINT_SERVICE);
PerformanceHintManager.Session hintSession =
performanceHintManager.createHintSession(tids, targetFpsNanos);
אם צריך, מגדירים שרשורים
תאריך הפרסום:
Android 11 (רמת API 34)
משתמשים בפונקציה setThreads
של PerformanceHintManager.Session
כשרוצים להוסיף בהמשך שרשורים אחרים. לדוגמה, אם יוצרים את השרשור של הפיזיקה מאוחר יותר וצריך להוסיף אותו לסשן, אפשר להשתמש ב-API setThreads
הזה.
C++
auto tids = thread_ids.data();
std::size_t size = thread_ids_.size();
APerformanceHint_setThreads(hint_session, tids, size);
Java
int[] tids = new int[3];
// add all your thread IDs. Remember to use android.os.Process.myTid() as that
// is the linux native thread-id.
// Thread.currentThread().getId() will not work because it is jvm's thread-id.
hintSession.setThreads(tids);
אם אתם מטרגטים רמות API נמוכות יותר, תצטרכו להרוס את הסשן וליצור סשן חדש בכל פעם שתצטרכו לשנות את מזהי השרשור.
דיווח על משך העבודה בפועל
המערכת עוקבת אחרי משך הזמן בפועל שנדרש להשלמת העבודה בננו-שניות, ומדווחת על כך בסיום העבודה בכל מחזור. לדוגמה, אם מדובר בשרשורי הרינדור, צריך להפעיל את הפונקציה הזו בכל פריים.
כדי לקבל את השעה בפועל באופן מהימן, משתמשים ב:
C++
clock_gettime(CLOCK_MONOTONIC, &clock); // if you prefer "C" way from <time.h>
// or
std::chrono::high_resolution_clock::now(); // if you prefer "C++" way from <chrono>
Java
System.nanoTime();
לדוגמה:
C++
// All timings should be from `std::chrono::steady_clock` or `clock_gettime(CLOCK_MONOTONIC, ...)`
auto start_time = std::chrono::high_resolution_clock::now();
// do work
auto end_time = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::nanoseconds>(end_time - start_time).count();
int64_t actual_duration = static_cast<int64_t>(duration);
APerformanceHint_reportActualWorkDuration(hint_session, actual_duration);
Java
long startTime = System.nanoTime();
// do work
long endTime = System.nanoTime();
long duration = endTime - startTime;
hintSession.reportActualWorkDuration(duration);
עדכון משך העבודה המתוכנן כשצריך
בכל פעם שמשך העבודה של היעד משתנה, למשל אם המשתמש בוחר יעד שונה של פריימים לשנייה, צריך להפעיל את השיטה updateTargetWorkDuration
כדי לעדכן את המערכת, כך שמערכת ההפעלה תוכל להתאים את המשאבים בהתאם ליעד החדש. לא צריך להפעיל אותה בכל פריים, אלא רק כשמשך היעד משתנה.
C++
APerformanceHint_updateTargetWorkDuration(hint_session, target_duration);
Java
hintSession.updateTargetWorkDuration(targetDuration);