תזמון וטופולוגיית מעבדים¶
נניח שפעולה לקחה שנייה. האם ה־CPU עבד עליה שנייה? אולי היא חיכתה לדיסק, אולי ה־thread היה ready אך לא קיבל מעבד, ואולי היא ישנה במפורש. מדידת זמן קיר לבדה אינה מסבירה את הסיבה.
המתזמן מחליט איזה thread מוכן ירוץ על איזה מעבד לוגי. ההחלטה מושפעת מעדיפויות, זמינות, טופולוגיה ומדיניות מערכת. שינוי affinity או priority לפני שמבינים את ההמתנה יכול להפוך את המערכת לאטית יותר.
מודדים זמן קיר וזמן CPU בנפרד¶
התוכנית הבאה מבצעת חישוב ואז ישנה. היא אוספת זמני kernel ו־user של ה־thread הנוכחי ומשווה אותם לזמן שחלף.
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <cstdint>
#include <cstdio>
std::uint64_t ticks(FILETIME value) {
return (static_cast<std::uint64_t>(value.dwHighDateTime) << 32) |
value.dwLowDateTime;
}
int main() {
FILETIME creation{}, exit{}, kernel_before{}, user_before{};
FILETIME kernel_after{}, user_after{};
if (!GetThreadTimes(GetCurrentThread(), &creation, &exit,
&kernel_before, &user_before)) return 1;
const ULONGLONG wall_before = GetTickCount64();
std::uint64_t value = wall_before | 1;
for (unsigned i = 0; i < 2000000; ++i) {
value ^= value << 13;
value ^= value >> 7;
value ^= value << 17;
}
Sleep(250);
const ULONGLONG wall_after = GetTickCount64();
if (!GetThreadTimes(GetCurrentThread(), &creation, &exit,
&kernel_after, &user_after)) return 2;
const auto cpu = ticks(kernel_after) - ticks(kernel_before) +
ticks(user_after) - ticks(user_before);
std::printf("wall_ms=%llu cpu_ms=%.3f result=%llu\n",
static_cast<unsigned long long>(wall_after - wall_before),
static_cast<double>(cpu) / 10000.0,
static_cast<unsigned long long>(value));
std::printf("groups=%u logical=%lu\n",
static_cast<unsigned>(GetActiveProcessorGroupCount()),
GetActiveProcessorCount(ALL_PROCESSOR_GROUPS));
}
ה־FILETIME של זמני CPU נמדד ביחידות של מאה ננו־שניות, אך יחידת הייצוג אינה מבטיחה דיוק מדידה של מאה ננו־שניות. גם GetTickCount64 אינו הכלי לכל benchmark קצר. הניסוי מכוון להבדל גדול בין חישוב להמתנה.
ה־Sleep מוסיף זמן קיר בלי לדרוש שה־thread יצרוך CPU לאורך כל ההמתנה. ההפרש בין זמן קיר לזמן CPU עדיין אינו אומר בדיוק למה ה־thread לא רץ; בשביל זה צריך ציר זמן של scheduling.
עדיפות אינה האצה של ההוראות¶
העדיפות משפיעה על בחירת thread מוכן. היא אינה גורמת להוראות שלו לבצע יותר עבודה בכל מחזור, ואינה הופכת המתנה ל־I/O לחישוב. תהליכון בעדיפות גבוהה שמחכה לנעילה עדיין תלוי במי שמחזיק אותה.
ב־Windows יש שילוב של priority class של התהליך ועדיפות יחסית של ה־thread. קיימים גם שינויים דינמיים ו־boosts בהתאם למדיניות. מספרי quantum ומדיניות boost אינם קבועים לכל build ותצורה.
אין צורך להפעיל REALTIME_PRIORITY_CLASS בניסוי. שינוי כזה יכול לפגוע ביכולת המערכת להגיב, ואינו פתרון לשרת שממתין לנעילה. אפשר לחקור תזמון באמצעות עומס מוגבל ובעדיפויות רגילות או נמוכות.
היפוך עדיפויות מתחיל בתלות¶
נניח ש־thread בעדיפות נמוכה מחזיק lock. תהליכון בעדיפות גבוהה צריך את ה־lock וממתין. בינתיים thread בינוני שרוצה CPU יכול להפריע לנמוך להתקדם, וכך הגבוה מחכה בעקיפין למי שנמצא מתחתיו.
המנגנונים שבהם Windows מנסה להתמודד עם מצבים כאלה תלויים בסוג ההמתנה ובמדיניות. אין להסיק שכל lock מקבל priority inheritance מלא עם אותם כללים. התכנון צריך לצמצם זמן החזקת נעילות ולהימנע מעבודה לא צפויה תחת lock.
מעבד לוגי אינו בהכרח ליבה עצמאית¶
מערכת יכולה להכיל כמה packages, כמה cores וכל אחד מהם יכול לחשוף כמה logical processors. קיימות גם מערכות הטרוגניות שבהן ליבות שונות מספקות מאפיינים שונים. מספר threads ששווה למספר המעבדים הלוגיים אינו מבטיח ניצול מיטבי לכל עומס.
במערכות גדולות קיימים processor groups. התנהגות ברירת המחדל של פיזור בין groups השתנתה בגרסאות Windows, ולכן קוד שקובע mask בן 64 ביט צריך להבין באיזה group הוא פועל. אין לפרש את הגבול של mask כגבול אוניברסלי של המחשב.
ב־NUMA, מיקום הזיכרון ביחס למעבד משפיע על עלות גישה. הצמדת thread לליבה יכולה לשפר locality במקרה אחד ולהגביל איזון עומס במקרה אחר. בפרק הזיכרון נבדוק first touch ומיפוי דפים בנפרד.
הצמדה למעבד היא מגבלה, לא הבטחת זמן¶
ה־affinity mask מגביל היכן thread רשאי לרוץ. הוא אינו שומר מעבד פנוי עבורו ואינו מבטיח שלא יופסק. מנגנוני CPU sets מספקים דרך נוספת לבטא בחירה שמתחשבת במדיניות מערכת, אך גם להם יש יחסי קדימות ומגבלות.
בניסוי עם group יחיד אפשר להשוות ברירת מחדל להצמדה זמנית למעבד מותר אחד, תוך שמירת המסכה הקודמת והחזרתה. בודקים כשל בכל API ומבצעים עומס מוגבל. אם המחשב אינו עומד בהנחות הניסוי, מנתחים trace מוכן או מתאימים את הקוד.
ציר הזמן מסביר את המספר¶
באיסוף ETW עם אירועי context switch ו־ready, ובניתוח ב־WPA, מחפשים מתי thread נעשה מוכן, מתי התחיל לרוץ ועל מה המתין. חשוב להפריד ready delay מזמן שבו הוא כלל אינו מוכן בגלל I/O או lock.
המדידה הטובה מציגה סיבה לשינוי: פחות מעבר בין מעבדים, פחות המתנה לנעילה או שינוי בעומס. מספר זמן כולל ללא ציר זמן יכול להראות שיפור מקרי שנעלם בריצה הבאה.
לקריאה נוספת: עדיפויות תזמון, קבוצות מעבדים ובחירת CPU sets.