תאריך פרסום:
Android 12 (רמת API 31) – Performance Hint API
Android 13 (רמת API 33) – Performance Hint Manager ב-NDK API
(תצוגה מקדימה) Android 15 (DP1) – reportActualWorkDuration()
עם טיפים לגבי ביצועים של המעבד (CPU), המשחק יכול להשפיע על הביצועים הדינמיים של המעבד (CPU) כדי להתאים אותו טוב יותר לצרכים שלו. ברוב המכשירים, מערכת Android מבצעת התאמות באופן דינמי מהירות השעון של המעבד (CPU) וסוג הליבה של עומס עבודה (workload) על סמך הדרישות הקודמות. אם עומס עבודה משתמש ביותר משאבי מעבד (CPU), מהירות השעון מוגברת בסופו של דבר עומס העבודה מועבר לליבה גדולה יותר. אם עומס העבודה משתמש בפחות משאבים מסוימים, מערכת Android מפחיתה את הקצאת המשאבים. בעזרת ADPF, או משחק מסוים יכולים לשלוח אות נוסף לגבי הביצועים ותאריכי היעד שלהם. הזה עוזר למערכת לבצע אופטימיזציה באופן אגרסיבי יותר (שיפור ביצועים) ולהוריד את שעונים מהר כשעומס העבודה הושלם (חיסכון בצריכת חשמל).
מהירות השעון
כשמכשירי Android משנים באופן דינמי את מהירות השעון במעבד (CPU), התדר יכול
לשנות את ביצועי הקוד. עיצוב קוד שעומד בקריטריונים של שעון דינמי
המהירות חשובה לקבלת ביצועים מקסימליים, תוך שמירה על טמפרטורה תרמית בטוחה
תוך שימוש יעיל בחשמל. לא ניתן להקצות ישירות תדרים במעבד (CPU)
בקוד של האפליקציה. כתוצאה מכך, דרך נפוצה שבה אפליקציות מנסות לפעול במהירות גבוהה יותר
מהירויות השעון של המעבד (CPU) הן הרצת לולאה עמוסה בשרשור ברקע, כך שעומס העבודה
נראה תובעני יותר. זוהי שיטה לא מומלצת כי היא מבזבזת אנרגיה ומתגברת
העומס התרמי על המכשיר כשהאפליקציה לא משתמשת בפועל בהרשאה
המשאבים. ה-API של המעבד (CPU) PerformanceHint
נועד לטפל בבעיה הזו. על ידי
מודיעים למערכת את משך העבודה בפועל ואת משך היעד של העבודה,
מערכת Android תוכל לקבל סקירה כללית של צורכי המעבד (CPU) של האפליקציה ולהקצות אותם
את המשאבים שלכם בצורה יעילה. זה יוביל לביצועים אופטימליים תוך שימוש יעיל בחשמל
רמת הצריכה.
סוגי ליבה
סוגי הליבה של המעבד (CPU) שבהם המשחק פועל הם גם ביצועים חשובים נוספים בשקלול. במכשירי Android, בדרך כלל משנים את ליבת המעבד (CPU) שמוקצית לשרשור על סמך ההתנהגות של עומסי העבודה לאחרונה. הקצאת הליבה של המעבד (CPU) גבוהה עוד יותר מורכבים ב-SoC עם כמה סוגי ליבות. בחלק מהמכשירים האלה, ניתן להשתמש בליבות רק לזמן קצר מבלי להיכנס לסביבה תרמית שאינה בר קיימא .
אסור לנסות להגדיר את תחום העניין המרכזי של המעבד (CPU) מהסיבות הבאות:
- סוג הליבה המתאים ביותר לעומס עבודה משתנה בהתאם לדגם המכשיר.
- רמת הקיימוּת של הרצת ליבות גדולות משתנה בהתאם ל-SoC ולמקורות הבאים: תמיסות תרמיות שמסופקות על ידי כל דגם של מכשיר.
- ההשפעה הסביבתית על המצב התרמי עשויה לסבך עוד יותר את הליבה בחירה. לדוגמה, מצב מזג האוויר או נרתיק לטלפון עשויים לשנות את מצב התרמי של מכשיר.
- בחירת הליבה לא יכולה לכלול מכשירים חדשים עם ביצועים נוספים יכולות תרמיות. כתוצאה מכך, בדרך כלל מכשירים מתעלמים מהמעבד של המשחק זיקה.
דוגמה להתנהגות ברירת המחדל של מתזמן המשימות ב-Linux
PerformanceHint API מפשט יותר מזמני אחזור של DVFS
- אם המשימות צריכות לרוץ במעבד CPU מסוים, PerformanceHint API יודע איך לקבל את ההחלטה הזו בשמכם.
- לכן, אין צורך להשתמש בזיקה.
- מכשירים מגיעים עם טופולוגיות שונות. המאפיינים של ההספק החשמלי והתרמי הם הם שונים מדי מכדי להיחשף למפתח האפליקציות.
- לא ניתן להניח הנחות לגבי המערכת שעליה אתם מפעילים.
הפתרון
ADPF מספק את הפונקציה PerformanceHintManager
כדי שהמשחקים יוכלו לשלוח רמזים לגבי הביצועים ל-Android ביחס למהירות השעון של המעבד (CPU)
סוג הליבה. לאחר מכן, מערכת ההפעלה יכולה להחליט מהי הדרך הטובה ביותר להשתמש ברמזים על סמך ה-SoC.
התרמוסטט של המכשיר. אם האפליקציה שלך משתמשת ב-API הזה יחד עם תרמי
הוא יכול לספק למערכת ההפעלה רמזים יותר מושכלים במקום
לולאות עמוסות ושיטות תכנות אחרות שעלולות לגרום לויסות נתונים (throttle).
כך המשחק משתמש בטיפים לגבי ביצועים:
- יוצרים סשנים של רמזים לשרשורים מרכזיים עם התנהגות דומה. מוצרים לדוגמה:
- עיבוד השרשור ויחסי התלות שלו מקבלים סשן אחד
- ב-Cocos, ה-thread הראשי של המנוע ושרשור העיבוד מקבלים אחד סשן
- ב-Unity, משלבים את הפלאגין של Adaptive Performance Android Provider
- ב-Unreal, צריך לשלב את הפלאגין Unreal Adaptive Performance (ביצועים מותאמים של Unreal) ולהשתמש אפשרויות של מדרגיות כדי לתמוך בכמה רמות איכות
- שרשורי IO מקבלים סשן נוסף
- שרשורי אודיו מקבלים סשן שלישי
- עיבוד השרשור ויחסי התלות שלו מקבלים סשן אחד
- צריך לבצע את המשחק מוקדם יותר, לפחות 2 אלפיות השנייה ורצוי יותר מ-4 אלפיות השנייה לפני שסשן צריך להגדיל את משאבי המערכת.
- בכל סשן של רמזים, ניתן לחזות את משך הזמן הדרוש להרצה של כל סשן. משך הזמן הטיפוסי מקביל למרווח זמן בין פריימים, אבל האפליקציה יכולה להשתמש פרק זמן קצר יותר, אם עומס העבודה לא משתנה באופן משמעותי בין פריימים.
כך אפשר ליישם את התיאוריה:
אתחול PerformanceHintManager ו-createHintSession
הורדת המנהל באמצעות שירות המערכת ויצירת סשן של רמזים לשרשור שלך או קבוצת שרשורים שעובדים על אותו עומס עבודה.
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);
עדכון של יעד משך העבודה במקרה הצורך
בכל פעם שמשך היעד של עבודת היעד משתנה, לדוגמה אם השחקן בוחר
קצבי יעד FPS שונים, קוראים ל-updateTargetWorkDuration
שתיידע את המערכת כדי שמערכת ההפעלה תוכל להתאים את המשאבים
ליעד החדש. לא צריך לקרוא לו בכל פריים וצריך רק
קוראים לה כשמשך היעד משתנה.
C++
APerformanceHint_updateTargetWorkDuration(hint_session, target_duration);
Java
hintSession.updateTargetWorkDuration(targetDuration);