מדריך מעשי להשגת מרב זמן אמת רך ב-Windows רגיל

· עודכן בתאריך: · · פיתוח Windows, זמן אמת רך, תכנון, מדידה

כשבונים ב-Windows עיבוד מסוג “בעייתי אם הוא מתעכב” - כמו עיבוד מחזורי, עיבוד אודיו, עיבוד וידאו, מדידה ובקרת התקנים - נוצר לעיתים קרובות הרושם ש”זה כנראה קשה ב-Windows”. הרושם הזה נכון בחצי, ולא נכון בחצי השני: Windows אינו hard real-time OS, אבל אם מסדרים היטב תכנון, מימוש, מדידה ותפעול, אפשר להגיע למצב מעשי למדי כ-soft real-time.

מה שהמאמר הזה עוסק בו הוא Windows 10 / 11 רגיל, בלי הרחבות RTOS מיוחדות, דרייבר קרנל עצמאי, או בקר ייעודי. זהו נושא מעשי על עד כמה אפשר לצמצם השהיה ו-jitter באפליקציית user-mode על מחשב שולחני / נייד רגיל. באודיו, וידאו, בקרה מחזורית ואיסוף נתונים הפרטים שונים, אבל המקומות הנוטים להיות בעייתיים משותפים למדי, ולכן ריכזנו כאן את החלק המשותף בצורת רשימת בדיקה.

קהל היעד, ושפת דוגמאות הקוד

המאמר נכתב עבור מפתחים שבונים ב-Windows עיבוד מסוג “בעייתי אם הוא מתעכב” (בקרה מחזורית, אודיו/וידאו, מדידה, בקרת התקנים). מדובר בפיתוח אפליקציות user-mode; מימוש דרייברים במצב kernel אינו בתחום המאמר.

שפת דוגמאות הקוד מחולקת כך:

תוכן שפה מיקום
לולאה מחזורית, MMCSS, QoS של צריכת חשמל וכדומה - חלקים שקוראים ישירות ל-Win32 API C++‏ (Win32) 4.1,‏ 4.3,‏ 4.5
אותה קריאה ל-Win32 API אך מ-C# C#‏ (P/Invoke) 4.5
מדידת זמן, GC, נקודות לתשומת לב בהקצאה ‎.NET (C#) “בדיקת צד ‎.NET” בסעיף 4.4, וסעיף 5.2

הדיון בגורמים עד פרק 3, וגוף רשימת הבדיקה בפרק 4, אינם תלויי שפה. מי שמשתמש רק ב-C# יכול לקרוא את קוד ה-C++ פשוט כהסבר “איזה API קוראים ובאיזה סדר”.

תוכן עניינים

  1. קודם המסקנה (במשפט אחד)
    • 1.1. טבלה מהירה לפי טווח מחזור (מאיפה לקרוא)
  2. מה זה “זמן אמת רך” ב-Windows רגיל
    • 2.1. מה נקרא “Windows רגיל” במאמר הזה
    • 2.2. מה אפשר לעשות, ומאיפה זה נהיה קשה
    • 2.3. מונחים קודם, במשפט אחד
  3. הגורמים העיקריים להשהיה ו-jitter
    • 3.1. תזמון ועדיפות
    • 3.2. DPC / ISR ודרייברים
    • 3.3. page fault וזיכרון
    • 3.4. רזולוציית טיימר וניהול צריכת חשמל
    • 3.5. מעבר בין ליבות וחום
  4. רשימת בדיקה מעשית להפחתת השהיה ב-Windows רגיל
    • 4.1. לולאה מחזורית ושיטת המתנה
    • 4.2. fast path / slow path ותור באורך קבוע
    • 4.3. עדיפות / MMCSS / background mode
    • 4.4. זיכרון / GC / עלות ראשונית
    • 4.5. הגדרות צריכת חשמל / EcoQoS / רזולוציית טיימר
    • 4.6. הקצאת CPU / מעבר בין ליבות / חום
    • 4.7. הפרדת דרייבר / DPC / ISR / הפרעות חיצוניות
  5. מדידה והערכה
    • 5.1. מה מתעדים
    • 5.2. איך קוראים p99 / p99.9 / מקסימום
    • 5.3. באילו כלים מסתכלים
    • 5.4. שיטת הבדיקה
  6. חלוקה גסה בין השימושים
  7. סיכום
  8. מקורות

מפת הידע של המאמר

כדי לשאוף ל-soft real-time ב-Windows רגיל, לא דורשים hard real-time שמבטיח אפס החמצות דדליין, אלא מתכננים כך שההשהיה וה-jitter קטנים ולא נשברים גם כשמפספסים דדליין. ההמתנה המחזורית מבוססת על מדידה עם QueryPerformanceCounter ועל waitable timer ברזולוציה גבוהה, ורק בזמן הנדרש מעלים את רזולוציית הטיימר עם timeBeginPeriod. אם לא מבטלים במפורש עם Power Throttling את הנטייה ל-EcoQoS ואת ההתעלמות מרזולוציית הטיימר, זה הופך, לצד DPC/ISR,‏ page fault וויסות עקב חום, לגורם נוסף להגדלת ה-jitter. בעיבוד רציף כמו אודיו ווידאו,‏ MMCSS מונע חריגה מדדליין בהקצאת CPU בעדיפות, והקצאת CPU כדאי לנסות קודם עם ציון רך כמו CPU Sets לפני נעילה קשיחה בעדיפות גבוהה, וכלי ההפרדה לגורם הם WPR/WPA ו-LatencyMon.

מפת הידע של זמן אמת רך ב-Windows רגילתרשים המראה את הקשר בין waitable timer ו-MMCSS שתומכים בזמן אמת רך, את הקשר בין Power Throttling ל-EcoQoS, את המסלול שבו DPC/ISR ו-page fault יוצרים jitter, את השלבים מ-CPU Sets ועד נעילת CPU, ואת הקשר בין WPR/WPA ל-LatencyMon ככלי מדידה.משתמש במשתמש במשתמש באינו מתיישב עםמשתמש במונעעלול לגרום למשתמש במצמצםמצמצםעלול לגרום לשימוש לא מומלץ למשתמש בצריך לקדום לעלול לגרום לעלול לגרום לעלול לגרום לנבדק באמצעותנבדק באמצעותנבדק באמצעותזמן אמת רךQPC(QueryPerformanceCounter)טיימר בר-המתנה (waitable timer)‏timeBeginPeriod (בקשת רזולוציית טיימר)‏Power Throttling (ויסות צריכת חשמל)EcoQoS‏jitter (פיזור זמנים)MMCSS(Multimedia Class Scheduler Service)החמצת דדלייןעדיפות ת'רד ומחלקת עדיפותCPU Setsזיקת מעבד (CPU affinity)ויסות עקב חום (thermal throttling)DPC/ISR‏page fault (שגיאת דף)WPR/WPA(Windows Performance Recorder/Analyzer)LatencyMon

בתרשים, קו מלא מציין קשר שמתקיים תמיד וקו מקווקו מציין קשר מותנה (תנאי ההתקיימות מפורטים בהסבר של כל קשר בעמוד המפורט). רשימת כל הקשרים (סך הכול 20, עם אסמכתה ורמת ודאות) והגדרות המושגים המרכזיים מרוכזות בעמוד המפורט של מפת הידע (ביפנית). נתונים: JSON-LD / Turtle

1. קודם המסקנה (במשפט אחד)

  • מה שרודפים אחריו ב-Windows רגיל הוא לא הבטחת hard real-time, אלא מבנה של soft real-time “שקשה לו להתעכב, ולא נשבר גם כשהוא מתעכב”.
  • מה שהכי משפיע הוא לקצר את ה-hot path, להפוך אותו לאורך קבוע ולא-חוסם.
  • מפרידים בין fast path (איסוף / בקרה) ל-slow path (שמירה / תקשורת / UI), ומחברים ביניהם תור באורך קבוע.
  • מריצים את הלולאה המחזורית לא לפי Sleep(1), אלא לפי דדליין מוחלט.
  • בזרם רציף כמו אודיו או וידאו, בודקים קודם MMCSS.
  • למדידת זמן משתמשים ב-QueryPerformanceCounter (QPC), וב-‎.NET ב-Stopwatch.
  • להמתנה מעדיפים אירוע התקן או waitable timer ברזולוציה גבוהה.
  • את timeBeginPeriod משתמשים רק בזמן הנדרש. לא מתכננים בהנחה שהוא פעיל כל הזמן.
  • בתפעול בפועל יעילים חיבור לחשמל / מצב צריכת חשמל / הטיפול ב-EcoQoS / סידור עומס רקע.
  • מעריכים לא רק לפי הממוצע, אלא לפי p99 (הגבול שבו מתחיל להיראות 1 מתוך 100 המדידות האיטיות ביותר) / p99.9 / מקסימום / מספר החמצות / DPC / ISR / page fault / עומק תור.

במילים אחרות, ב-Windows רגיל הקטנת סיבות ההשהיה בתכנון עוזרת יותר מהעלאת עדיפות. עדיפות והגדרות צריכת חשמל חשובות, אבל הן לבדן לא יוצרות יציבות.

יציבות נוצרת מתכנון, לא מעדיפותתרשים המראה שקיצור ה-hot path והפיכתו לאורך קבוע ולא-חוסם מקטינה בתכנון את סיבות ההשהיה ומובילה למבנה שקשה לו להתעכב ולא נשבר, בעוד עדיפות והגדרות צריכת חשמל חשובות אך לא מספיקות לבדן.hot path קצר ובאורך קבועסיבות ההשהיה קטנות בתכנוןמבנה שקשה לו להתעכב ולא נשברכיוונון עדיפות והגדרות צריכת חשמלחשוב אך לא מספיק לבדו

איור 1: מה שהכי משפיע הוא לא כיוונון עדיפות, אלא צמצום סיבות ההשהיה עצמן בתכנון.

1.1. טבלה מהירה לפי טווח מחזור (מאיפה לקרוא)

זהו מאמר ארוך, ולכן נציג קודם טבלה מהירה כדי שתוכלו לקרוא רק את השורה הרלוונטית למקרה שלכם. תוכן שהיה בעבר בסוף (פרק 6) הועבר לכאן.

מחזור / דרישה המבנה הראשוני להרכיב הסעיפים החשובים לקריאה
ברמת 10-20ms, אפשר לספוג תנודות מדי פעם הפרדת fast path / slow path, תור באורך קבוע, עדיפות רגילה עד מעט גבוהה, מונע אירועים. לרוב זה מספיק 4.1,‏ 4.2
ברמת 1-5ms, רוצים לעמוד בזה באופן רציף בנוסף לעיל: ביטול הקצאות ב-hot path, ת’רד ייעודי, MMCSS או כיוונון עדיפות זהיר, waitable timer ברזולוציה גבוהה, חיבור לחשמל ובדיקת הגדרות צריכת חשמל 4.1 עד 4.5
מתקרבים ל-פחות מ-1ms, ולא רוצים לפספס גם בהפעלה ארוכה ובעומס גבוה קשה מאוד עם user-mode לבד ב-Windows רגיל. כדאי לשקול קודם תכנון שמעביר את החלק הקריטי למקום אחר (קושחת ההתקן, בקר ייעודי, FPGA,‏ RTOS) 2.2,‏ 6
רוצים הכול ביחד - GUI / יומן / תקשורת / DB לא לגרור הכול ב”תהליך אחד, לולאה אחת” - לחלק אחריות. נטיית העניינים בשלב המאוחר נוטה לשבור את הדדליין של השלב המוקדם 4.2,‏ 4.3,‏ 6

הפרדת הגורמים ושיטת המדידה משותפים לכל טווח (פרקים 3 ו-5).

2. מה זה “זמן אמת רך” ב-Windows רגיל

2.1. מה נקרא “Windows רגיל” במאמר הזה

Windows רגיל, כאן, מניח בערך את הדברים הבאים.

  • מחשב שולחני / נייד רגיל עם Windows 10 / 11
  • ללא הרחבת RTOS עצמאית
  • ללא פיתוח דרייבר קרנל עצמאי
  • אפליקציית user-mode רגילה
  • כיוונון עם Windows API והגדרות רגילות

כלומר, זה לא סיפור של “לבנות מכשיר ייעודי אחד כולו לבקרת זמן אמת”, אלא “עד כמה אפשר לצמצם באופן מעשי על מחשב Windows רגיל”.

היקף המאמרתרשים המראה שהמאמר עוסק באפליקציית user-mode על מחשב Windows רגיל שמכוונת לזמן אמת רך — השהיה נמוכה, פחות jitter וניטור החמצות דדליין — ושדרישה לאפס החמצות מחייבת RTOS, בקר ייעודי, FPGA או עיבוד בצד ההתקן.מחשב Windows 10 / 11 רגילאפליקציית user-modeשואפים ל-soft real-timeמנמיכים השהיהמקטינים jitterמנטרים החמצות דדליין ולא נשבריםרוצים להבטיח אפס החמצות דדלייןRTOS / בקר ייעודי / FPGA / עיבוד בצד ההתקן

איור 2: באפליקציית user-mode ב-Windows רגיל שואפים ל-soft real-time; הבטחת אפס החמצות דדליין שייכת לתחום ה-RTOS וכדומה.

2.2. מה אפשר לעשות, ומאיפה זה נהיה קשה

גם ב-Windows רגיל, אפשר ליצור באופן מעשי מצב של “קשה להתעכב” בעיבודים כמו אלה.

  • עיבוד מחזורי של כמה מילישניות עד עשרות מילישניות
  • הפעלה מבוססת מאגר לאודיו / וידאו
  • איסוף מחיישנים ולולאות בקרה
  • עיבוד מחזורי קבוע בסגנון PLC תוכנתי
  • צינור עיבוד בהשהיה נמוכה שרץ בת’רד נפרד מה-UI

עם זאת, “אפשר” כאן אינו אומר שאפשר לאפס לגמרי קפיצות השהיה מדי פעם, אלא המטרה היא בעיקר המצב הבא.

  • הנמכת ההשהיה הרגילה
  • הקטנת ה-jitter
  • לא להישבר גם כשמפספסים דדליין מדי פעם
  • היכולת לצפות בעובדה שפספסנו

לעומת זאת, כשהדרישה היא כזו, קשה מאוד לספק אותה עם user-mode ב-Windows רגיל בלבד.

  • רוצים להבטיח אפס החמצות דדליין
  • רוצים לשמור בעקביות על פחות ממאות מיקרו-שניות לאורך זמן
  • רוצים לגור יחד עם GUI כבד, רשת ואחסון
  • רוצים לעשות זאת תוך הפעלה על סוללה או עדיפות לחיסכון בחשמל
  • אסור שגם קפיצות שמקורן בדרייבר או בהתקן יופיעו

במקרים כאלה, בטוח יותר לשקול להעביר רק את החלק שבאמת דורש דיוק זמנים חמור לקושחת ההתקן, לבקר ייעודי, ל-FPGA או ל-RTOS.

המצב שרודפים אחריו ב-soft real-time וההחלטה מתי להעביר החוצהתרשים המראה שב-Windows רגיל שואפים להנמיך את ההשהיה הרגילה, להקטין jitter, ולא להישבר תוך יכולת ניטור כשמפספסים דדליין מדי פעם, ואילו דרישה חמורה כמו אפס החמצות דדליין שווה להעביר לחלק שרק בו נדרש דיוק זמנים.המצב שרודפים אחריו ב-soft real-timeהנמכת ההשהיה הרגילההקטנת ה-jitterלא נשברים גם כשמפספסים, וניתן לנטרדרישה של אפס החמצות דדלייןמעבירים ל-התקן או RTOS

איור 3: תוכן ה”אפשר” אינו אפס קפיצות, אלא מצב שקשה לו להתעכב, קשה לו להישבר, וניתן לנטר.

2.3. מונחים קודם, במשפט אחד

נסדר מראש את המשמעות של המונחים שמופיעים במאמר הזה.

מונח במשפט אחד הזווית המעשית
soft real-time תפיסה שמקבלת שיכולה להיות עיכוב מדי פעם, מקטינה את העיכוב, ולא נשברת גם כשהוא קורה זה מה שרודפים אחריו קודם ב-Windows רגיל
hard real-time עולם שבו רוצים להבטיח אפס החמצות דדליין לא מטרה מעשית ל-user-mode לבד ב-Windows רגיל
jitter תנודה במחזור או בזמן התגובה גם אם הממוצע טוב, jitter גדול לא יציב בתפעול בפועל
deadline miss (החמצת דדליין) העיבוד לא מסתיים עד הזמן המתוכנן לא מסתירים, סופרים, ומתעדים ביומן
p99 / p99.9 מדדים שמראים את הזנב האיטי p99 הוא “הגבול שבו מתחיל להיראות 1 מתוך 100 המדידות האיטיות ביותר”
DPC / ISR עיבוד בצד הקרנל סביב דרייברים והפרעות אם הוא ארוך, ת’רד ה-user-mode נאלץ להמתין
MMCSS מנגנון ב-Windows שמקצה CPU לעיבוד רגיש-זמן כמו אודיו / וידאו יעיל בעיבוד שלא רוצים שהמאגר שלו יתרוקן
QPC כינוי ל-QueryPerformanceCounter הבסיס למדידת זמן חלוף. לא שעון קיר אלא מונה ברזולוציה גבוהה
waitable timer (טיימר בר-המתנה) אובייקט קרנל שהופך ל-signaled בזמן שנקבע. אם מוסיפים CREATE_WAITABLE_TIMER_HIGH_RESOLUTION ל-CreateWaitableTimerExW, מתקבלת גרסה ברזולוציה גבוהה מתאים כבסיס להמתנה מחזורית יותר מ-Sleep (4.1)
EcoQoS מצב שסווג כ”מותר לתת עדיפות לחיסכון בחשמל”. יכול להוריד תדר CPU או להעביר לליבות חסכוניות בחשמל נמנעים ממנו בעיבוד רגיש-זמן. אם לא מציינים במפורש, מערכת ההפעלה מנחשת אוטומטית (4.5)
IGNORE_TIMER_RESOLUTION ציון “מותר להתעלם מבקשת רזולוציית הטיימר של התהליך” (PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION) אם זה פעיל, האפקט של timeBeginPeriod נעלם. ב-Windows 11 זה יכול לחול אוטומטית כשהחלון מוסתר (4.5)
CPU Sets מנגנון לציון רך מסוג “רוצים שהריצה תהיה בקבוצת הליבות הזו” עבור ת’רד או תהליך בודקים לפני נעילה לליבה ספציפית (4.6)
ETW / WPR / WPA תשתית המעקב הסטנדרטית של Windows (ETW), וכלי ההקלטה שלה (WPR) / ה-GUI לניתוח (WPA) משמש לחפירה ב-context switch,‏ DPC / ISR ו-page fault (5.3)
LatencyMon כלי צד-שלישי לבדיקת השהיה שמקורה בדרייבר סוקר את זמני ה-DPC / ISR לפי דרייבר כדי לזהות חשוד (5.3)

3. הגורמים העיקריים להשהיה ו-jitter

הסיבה שבגללה עיבוד מחזורי מתעכב ב-Windows רגיל, מגיעה בדרך כלל לאחד מהענפים בתרשים הזה.

גורמים לעיכוב בעיבוד מחזוריתרשים המונה חמישה כיווני חקירה כשעיבוד מחזורי מתעכב: תזמון ועדיפות, DPC ו-ISR ודרייברים, page fault וזיכרון, רזולוציית טיימר וניהול צריכת חשמל, ומעבר בין ליבות וחום.עיבוד מחזורי מתעכבתזמון / עדיפותDPC / ISR / דרייברpage fault / זיכרוןרזולוציית טיימר / ניהול צריכת חשמלמעבר בין ליבות / חום

איור 4: הסיבות לעיכוב עיבוד מחזורי מגיעות לחמש מערכות - תזמון, DPC/ISR, זיכרון, טיימר וצריכת חשמל, ומעבר בין ליבות וחום.

3.1. תזמון ועדיפות

סדר הריצה של ת’רדים ב-Windows נקבע לפי עדיפות. באותה עדיפות רצים לפי round-robin, וכשת’רד בעדיפות גבוהה יותר הופך לניתן להרצה, ת’רד בעדיפות נמוכה יותר נדחק הצידה.

כלומר, גם אם כותבים ת’רד מחזורי ברצינות, יכולים באופן שגרתי לרוץ לפניו:

  • ת’רד אחר
  • תהליך אחר
  • עיבוד פנימי של מערכת ההפעלה
  • מוצר אבטחה
  • עיבוד עזר של התקן
  • סנכרון ברקע
דחיקה על ידי תזמון לפי עדיפותתרשים המראה שת'רדים באותה עדיפות רצים בסבב round-robin, וכשת'רד בעדיפות גבוהה יותר הופך לניתן להרצה, ת'רד בעדיפות נמוכה נדחק הצידה, ולכן זה שגרתי שתהליך אחר או עיבוד פנימי של מערכת ההפעלה ירוצו לפני ת'רד מחזורי.ת'רדים באותה עדיפותרצים בסבב round-robinעדיפות גבוהה יותר הופכת לניתנת להרצהעדיפות נמוכה נדחקת הצידהלמשל תהליך אחר או עיבוד פנימי של מערכת ההפעלה

איור 5: גם כשכותבים ת’רד מחזורי ברצינות, זה שגרתי שעבודה בעדיפות גבוהה יותר תרוץ לפניו.

3.2. DPC / ISR ודרייברים

זו נקודה חשובה מאוד. גם אם מסדרים את העדיפות בצד האפליקציה, אם DPC (Deferred Procedure Call) או ISR (Interrupt Service Routine) ארוכים, ת’רד ה-user-mode לא יכול לרוץ באותו זמן.

ההתקנים והדרייברים שנוטים לגרום לזה הם בערך אלה.

  • USB
  • Wi-Fi / Bluetooth
  • אחסון
  • אודיו
  • GPU
  • ACPI וסביבת צריכת החשמל

גם כשהקוד של האפליקציה לא רע, אפשר להיעצר בגלל צרכי הדרייבר או החומרה. כאן, אם חושבים “אם מעלים עוד יותר את עדיפות האפליקציה, ננצח” - בדרך כלל נכשלים בכאב.

איך DPC ו-ISR עוצרים את ה-user-modeתרשים המראה שכשהתקנים כמו USB, Wi-Fi ו-GPU מפעילים ISR או DPC ארוכים בצד הקרנל, ת'רד ה-user-mode לא יכול לרוץ באותו זמן, וגם העלאת עדיפות האפליקציה לא מנצחת את זה.התקנים כמו USB,‏ Wi-Fi,‏ GPUISR או DPC רצים בצד הקרנלבאותו זמן ה-user-mode לא יכול לרוץגם העלאת עדיפות לא מנצחת

איור 6: גם כשהקוד של האפליקציה לא רע, ה-user-mode נעצר בגלל צרכי הדרייבר או החומרה.

3.3. page fault וזיכרון

כשמתרחש page fault (העמוד הדרוש לא נמצא בזיכרון, ויוצאים להביא אותו) ב-hot path, ההשהיה גדלה בבת אחת.

נציין תבניות שכדאי במיוחד להימנע מהן.

  • commit של עמוד בגישה הראשונה
  • טעינה מושהית (lazy load)
  • page-in של קובץ ממופה לזיכרון
  • הקצאה דינמית מעבר לנדרש
  • אובייקטים גדולים או ערמה מפוצלת

בגוף העיבוד המחזורי, נכון להקצות מראש את הזיכרון הדרוש ולגעת בו פעם אחת בזמן ההפעלה.

השהיה הנגרמת מ-page fault ב-hot pathתרשים המראה שאם העמוד שנוגעים בו ב-hot path לא נמצא בזיכרון, יוצאים להביא אותו ב-page fault וההשהיה גדלה בבת אחת, ולכן עדיף להקצות מראש ולגעת פעם אחת בזמן ההפעלה.כןלאנוגעים בזיכרון ב-hot pathהעמוד נמצא בזיכרון?ממשיכים כרגיליוצאים להביא אותו ב-page faultההשהיה גדלה בבת אחתהקצאה מראש ונגיעה פעם אחת בהפעלה

איור 7: ההשהיה קופצת לפי אם מתרחש page fault. הפתרון הוא הקצאה מראש וחימום בזמן ההפעלה.

3.4. רזולוציית טיימר וניהול צריכת חשמל

“רוצה להריץ כל 1ms, אז Sleep(1)” - כמעט תמיד לא עובד. דיוק ההמתנה ב-Windows מושפע מרזולוציית הטיימר, מהתזמון, וממצב צריכת החשמל.

מעבר לזה, הגדרה שמעלה את רזולוציית הטיימר משפרת מעט את דיוק ההמתנה, אבל יש לה תופעות לוואי על צריכת החשמל והתנהגות המערכת כולה - נקודה שאי אפשר להתעלם ממנה.

3.5. מעבר בין ליבות וחום

כשת’רד עובר בין ליבות, נוצר חימום מחדש של המטמון. זה עצמו מטופל לרוב היטב על ידי מערכת ההפעלה, אבל בסביבה עמוסה זה הופך לגורם תנודה.

מעבר לזה, בהפעלה ארוכת טווח גם החום אי אפשר להתעלם ממנו. כשמופעל ויסות עקב חום (thermal throttling), המחזור שהיה יציב עד אז יכול להישבר.

המסלול שבו מעבר בין ליבות וחום שוברים את המחזורתרשים המראה שמעבר ת'רד בין ליבות גורם לחימום מחדש של המטמון שהופך לגורם תנודה בסביבה עמוסה, ובהפעלה ארוכת טווח הצטברות חום מפעילה ויסות עקב חום ששובר את המחזור שהיה יציב.ת'רד עובר בין ליבותחימום מחדש של המטמוןבסביבה עמוסה - גורם תנודהבהפעלה ארוכה מצטבר חוםויסות עקב חוםהמחזור היציב נשבר

איור 8: מעבר בין ליבות פוגע ביציבות דרך jitter, וחום פוגע בה דרך ויסות, שניהם בהפעלה ארוכת טווח.

4. רשימת בדיקה מעשית להפחתת השהיה ב-Windows רגיל

מכאן זה החלק המעשי. לגורמים שראינו בסעיף הקודם, נרכז בצורת רשימת בדיקה מה בודקים, ממה נמנעים, ומה מחליטים מראש ב-Windows רגיל.

4.1. לולאה מחזורית ושיטת המתנה

קודם כול, אנטי-דפוס טיפוסי הוא זה.

while (running)
{
    Sleep(1);
    Step();
}

זו לא לולאה של “מחזור 1ms”, אלא לולאה שבה ממתינים בערך 1ms ומעלה, ומעל זה מוסיפים את זמן הריצה של Step(). בנוסף, חריגת ההמתנה מצטברת כמו שהיא.

המתנה יחסית מול דדליין מוחלטתרשים המשווה בין לולאה מבוססת Sleep יחסי, שבה שגיאת ההמתנה וזמן הריצה מצטברים בהדרגה, לבין לולאה מבוססת דדליין מוחלט שמקדמת את הזמן הבא בקבוע ולכן קשה לצבור בה drift.מבוסס דדליין מוחלטמבוסס זמן יחסיWaitUntil(next - margin)next += periodspin קצר במידת הצורךFastStep()Step()Sleep(1)שגיאת ההמתנה וזמן הריצה מצטברים בהדרגהקשה לצבור drift

איור 9: לולאה מבוססת זמן יחסי שמסתמכת על Sleep(1) צוברת שגיאה, ולולאה מבוססת דדליין מוחלט קשה לה לצבור drift.

רשימת בדיקה

  • לא מבססים את הלולאה המחזורית על Sleep(1)
  • מריצים את המחזור לפי דדליין מוחלט של next += period
  • להמתנה מעדיפים אירוע התקן או waitable timer (טיימר בר-המתנה)
  • רק כיוונון עדין אחרון מוגבל ל-busy-spin (המתנה בסיבוב ריק) קצר מאוד
  • את timeBeginPeriod משתמשים רק בזמן הנדרש, ומחזירים כשמסיימים
  • בודקים את ההתנהגות גם במצב ממוזער / מוסתר / לא נראה

הלולאה המחזורית יציבה יותר כשהיא רצה לפי דדליין מוחלט ולא לפי זמן יחסי.

int64_t next = QpcNow() + periodTicks;

while (running)
{
    WaitUntil(next - wakeMarginTicks);

    while (QpcNow() < next)
    {
        CpuRelax(); // רק בסוף, spin קצר
    }

    int64_t started = QpcNow();
    FastStep();
    int64_t finished = QpcNow();

    RecordTiming(next, started, finished);

    next += periodTicks;

    while (finished > next)
    {
        ++missedDeadlines;
        next += periodTicks;
    }
}

4.2. fast path / slow path ותור באורך קבוע

יסוד המבנה הוא לשים ב-fast path רק “עבודה רגישה לדדליין”, ולהוציא את השאר ל-slow path.

הפרדה בין fast path ל-slow pathתרשים המראה שאירוע מההתקן מטופל ב-fast path עם עבודה מינימלית, עובר דרך תור באורך קבוע אל slow path שמבצע שמירה, שליחה, UI וסיכום, ושה-fast path מתעד גם lateness, miss ועומק תור.התקן / אירוע איסוףfast path: איסוף, בקרה, העתקה מינימליתתור באורך קבועslow path: שמירה, שליחה, UI, סיכוםתיעוד lateness / miss / עומק תור

איור 10: ב-fast path שמים רק עבודה רגישה לדדליין, ומוציאים ל-slow path דרך תור באורך קבוע.

מה שעושים ב-fast path מצמצמים בערך לזה.

  • איסוף נתונים
  • חישוב ערך בקרה
  • העתקה מינימלית נדרשת
  • חותמת זמן
  • הזנה לתור
  • תיעוד miss / overrun

כל השאר מורידים ל-slow path.

רשימת בדיקה

  • אין כתיבה לקובץ, שליחה ברשת, כתיבה ל-DB ב-hot path
  • אין יומן כבד, Flush, או RPC סינכרוני ב-hot path
  • מפרידים בבירור בין fast path ל-slow path לפי ת’רד או אחריות
  • התור באורך קבוע
  • מדיניות למקרה שהתור מלא הוחלטה מראש
  • מנטרים מספר miss, מספר drop, ועומק תור
  • עדכון UI וריכוז יומן מופרדים לתדירות נמוכה

כשהתור מתמלא, בטוח יותר לא להשאיר את המדיניות מעורפלת.

מדיניות כשהתור מתמלאתרשים המראה שלוש מדיניות אפשריות כשהתור מלא, לפי מה שחשוב לשמור: מחיקת ישן ושמירת העדכני, התראה ועצירה או בקרת המקור, או הפלת ישן עם תיעוד מספר ה-drop בלבד.הערך העדכני חשובכל הרשומות חשובותלצורכי יומןהתור מלאמה שומרים?מוחקים ישן ומשאירים עדכניהתראה / עצירה / בקרת המקורמפילים ישן, ומתעדים רק את מספר ה-drop

איור 11: מה שומרים כשהתור מתמלא מוחלט מראש לפי מטרת השימוש.

4.3. עדיפות / MMCSS / background mode

יסוד העדיפות הוא לא להעלות הכול. ב-Windows רגיל, “מעלים רק את הת’רדים החשובים, ומורידים כראוי את עבודות הרקע” עובד טוב יותר. ‏background mode הוא מנגנון שמתייחס בעדיפות נמוכה לא רק ל-CPU אלא גם למשאבים כמו I/O.

חלוקת העבודה בין ת'רדים ועדיפויותתרשים המראה חלוקה של העבודה לת'רד רגיש לדדליין, לת'רד עבודה ולממשק המשתמש, עם עדיפות גבוהה יותר או MMCSS לראשון, מצב רקע לשני ועדיפות רגילה לשלישי, ובלי לעבור מלכתחילה ל-REALTIME_PRIORITY_CLASS.מחלקים עבודהת'רד רגיש לדדלייןשמירה / שליחה / דחיסה / סיכוםUIעדיפות גבוהה יותר או MMCSS, במידת הצורךbackground mode / עדיפות נמוכה יותרעדיפות רגילהלא הופכים ל-REALTIME_PRIORITY_CLASS מלכתחילה

איור 12: אופן חלוקת העדיפות - מעלים רק את הת’רד הרגיש לדדליין, ומורידים כראוי את עבודות הרקע.

רשימת בדיקה

  • לא הופכים את כל הת’רדים לעדיפות גבוהה
  • מעלים רק את הת’רד שבאמת רגיש לזמן
  • עבודות רקע כמו שמירה, שליחה, דחיסה וסנכרון מורדות ל-background mode
  • בעיבוד מאגר רציף כמו אודיו, וידאו, צילום או ניגון, שוקלים MMCSS
  • חושבים קודם ברמת הת’רד, לא ברמת התהליך כולו
  • לא משתמשים ב-REALTIME_PRIORITY_CLASS עד שהצורך ברור

‏MMCSS (Multimedia Class Scheduler Service) יעיל במיוחד עבור עיבוד כמו אודיו / וידאו שרוצים “למלא מאגר בתוך זמן קצוב”. זה תואם יותר לתכנון של Windows מאשר פשוט להריץ ת’רד בעדיפות גבוהה כל הזמן.

הרגשת הקוד נראית כך בערך.

DWORD taskIndex = 0;
HANDLE avrt = AvSetMmThreadCharacteristicsW(L"Pro Audio", &taskIndex);
if (!avrt)
{
    throw std::runtime_error("AvSetMmThreadCharacteristicsW failed");
}

// כאן רצה הלולאה הרגישה לזמן

if (!AvRevertMmThreadCharacteristics(avrt))
{
    throw std::runtime_error("AvRevertMmThreadCharacteristics failed");
}
רישום וביטול ת'רד ל-MMCSSתרשים המראה שלפני הלולאה הרגישה לזמן נרשמים ל-MMCSS עם AvSetMmThreadCharacteristicsW, ואחרי שהלולאה מסתיימת חוזרים עם AvRevertMmThreadCharacteristics, וש-MMCSS מקצה CPU בעדיפות לעיבוד רגיש-זמן.AvSetMmThreadCharacteristicsWמריצים לולאה רגישה לזמןAvRevertMmThreadCharacteristicsMMCSS מקצה CPU בעדיפות

איור 13: משתמשים ב-MMCSS כזוג של רישום וביטול. בעיבוד מאגר רציף, זה תואם יותר את תכנון Windows מהרצה קבועה בעדיפות גבוהה.

4.4. זיכרון / GC / עלות ראשונית

אם משתמשים ב-hot path בכל פעם ב-new /‏ malloc /‏ List<T>.Add / חיבור מחרוזות /‏ LINQ, מוקדם או מאוחר צרכי האיסוף או הסידור מחדש יעלו לפני השטח. ‏GC (איסוף אשפה) בעצמו אינו רע, אך אם כותבים קוד עתיר הקצאות, ההשפעה שלו מופיעה כ-jitter.

חימום לפני המדידהתרשים המראה שאחרי ההפעלה מקצים את המאגרים הדרושים, נוגעים בהם פעם אחת כדי לחמם עמודים, מסיימים JIT, טעינת DLL ו-I/O ראשוני, ורק אחר כך מודדים או מריצים בפועל.הפעלהמקצים את המאגרים הדרושיםנוגעים פעם אחת כדי לחמם עמודיםמסיימים JIT, טעינת DLL, ו-I/O ראשונירק אחר כך מודדים / מריצים בפועל

איור 14: מקצים מאגרים, מחממים עמודים, ומסיימים עלות ראשונית בזמן ההפעלה, ורק אז נכנסים למדידה / הפעלה אמיתית.

רשימת בדיקה

  • אין הקצאה / שחרור זיכרון בכל פעם ב-hot path
  • המאגרים הדרושים מוקצים מראש בזמן ההפעלה
  • נוגעים פעם אחת בזמן ההפעלה כדי לחמם עמודים
  • ה-JIT הראשוני, טעינת ה-DLL הראשונה, וה-I/O הראשוני לא מעורבים במדידה האמיתית
  • לא מגדלים מבנה ענק או יומן באורך משתנה בתוך הלולאה
  • גם אם משתמשים ב-VirtualLock, זה מוגבל לאזור קריטי קטן מאוד

בדיקת צד ‎.NET

  • למדידת זמן משתמשים ב-Stopwatch / Stopwatch.GetTimestamp()
  • אין LINQ, חיבור מחרוזות, ToString(), או יצירת יומן ענק ב-hot path
  • לא מכניסים async/await ל-hot path
  • מעריכים בנפרד לפני ואחרי החימום

4.5. הגדרות צריכת חשמל / EcoQoS / רזולוציית טיימר

זה חלק לא בולט אבל יעיל. גם אם מסדרים את הקוד, אם בקרת צריכת החשמל ברמה הגבוהה יותר פעילה בעוצמה, התוצאה לא תהיה יציבה.

הגדרות צריכת החשמלתרשים המונה את מה שבודקים בסביבת צריכת החשמל של Windows רגיל: הפעלה על חשמל AC, מצב צריכת חשמל המכוון לביצועים, תוכנית חשמל ייעודית לסביבת ייצור, הימנעות מ-EcoQoS בתהליך רגיש-זמן, ובדיקת הטיפול בבקשת רזולוציית הטיימר.סביבת צריכת החשמל ב-Windows רגילמפעילים על חשמל ACמצב צריכת חשמל: כיוון לביצועים מיטבייםבמידת הצורך, תוכנית חשמל ייעודית לסביבת ייצורנמנעים מ-EcoQoS בתהליך רגיש-זמןבודקים את הטיפול בבקשת רזולוציית הטיימר

איור 15: הנקודות שבודקים בסביבת צריכת החשמל - חיבור, מצב צריכת חשמל, EcoQoS, וטיפול בבקשת רזולוציית הטיימר.

רשימת בדיקה

  • הערכת הייצור נעשית קודם על חשמל AC
  • [הגדרות] > [מערכת] > [חשמל וסוללה] > [מצב צריכת חשמל] מכוונים לכיוון ביצועים מיטביים
  • לא משתמשים במצב חיסכון בסוללה / חיסכון בחשמל בזמן ההפעלה
  • בודקים מצבי שקט / חיסכון / סוללה של כלים עצמאיים של יצרן
  • לא משאירים תהליך רגיש-זמן ב-EcoQoS (QoS מוטה חיסכון) בלי כוונה
  • IGNORE_TIMER_RESOLUTION לא פעיל בצד תהליך רגיש-זמן
  • בודקים שהאפקט של בקשת רזולוציית הטיימר לא משתנה כשממזערים / מסתירים
  • מפרידים הגדרות צריכת חשמל לשימוש יומיומי מהגדרות לייצור / מדידה / הדגמה

timeBeginPeriod שימושי כשמשתמשים בו בזהירות, אבל הוא לא תרופת פלא.

  • קוראים לו ממש לפני הצורך
  • כשמסיימים, מחזירים עם timeEndPeriod
  • מ-Windows 10 version 2004 ואילך, ההתנהגות אינה גלובלית מלאה כפי שהייתה בעבר
  • ב-Windows 11, כשתהליך עם חלון מוסתר לגמרי / ממוזער / לא נראה / לא נשמע, ייתכן שהרזולוציה הגבוהה לא מובטחת
  • העלאת הרזולוציה לא משפרת את דיוק ה-QPC

אם ההשפעה של צריכת חשמל או QoS מוטלת בספק, בודקים את מצב ה-power throttling עם SetProcessInformation.

PROCESS_POWER_THROTTLING_STATE state{};
state.Version = PROCESS_POWER_THROTTLING_CURRENT_VERSION;
state.ControlMask =
    PROCESS_POWER_THROTTLING_EXECUTION_SPEED |
    PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION;
state.StateMask = 0; // HighQoS (מוטה ביצועים) + כיבוד בקשת רזולוציית הטיימר

if (!SetProcessInformation(
        GetCurrentProcess(),
        ProcessPowerThrottling,
        &state,
        sizeof(state)))
{
    throw std::runtime_error("SetProcessInformation failed");
}

ControlMask הוא “אילו מנגנונים שולטים בעצמכם”, ו-StateMask הוא “האם מפעילים או מכבים את המנגנון הזה”. הדוגמה למעלה בוחרת שני מנגנונים לשליטה, וכובה את שניהם. כלומר, זו הצהרה שלא נופלים ל-EcoQoS (מוטה HighQoS), ושבקשת רזולוציית הטיימר לא מתעלמים ממנה. לעומת זאת, אם הופכים את ControlMask ל-0, שניהם חוזרים למערכת ההפעלה (התנהגות ברירת מחדל).

תפקיד שתי המסכות בהגדרת power throttlingתרשים המראה שבוחרים את המנגנון שנשלט על ידי ControlMask, וקובעים אם הוא פועל או כבוי עם StateMask, וכך אפשר להצהיר שלא נופלים ל-EcoQoS ושבקשת רזולוציית הטיימר לא מתעלמים ממנה.ControlMask בוחר יעד שליטהStateMask קובע פועל וכבויהצהרה שלא נופלים ל-EcoQoSבקשת רזולוציית הטיימר לא מתעלמים ממנהאם ControlMask הוא 0, חוזרים למערכת ההפעלה

איור 16: תפקיד שתי המסכות של SetProcessInformation. קודם בוחרים יעד שליטה, ואז מצהירים פועל/כבוי.

כשקוראים מ-C#

אם רוצים לעשות את אותו הדבר מ-C#, הצהרת ה-P/Invoke נראית כך.

// C# / .NET 8
using System.Runtime.InteropServices;

internal static class PowerQos
{
    [StructLayout(LayoutKind.Sequential)]
    private struct PROCESS_POWER_THROTTLING_STATE
    {
        public uint Version;
        public uint ControlMask;
        public uint StateMask;
    }

    private const uint PROCESS_POWER_THROTTLING_CURRENT_VERSION = 1;
    private const uint PROCESS_POWER_THROTTLING_EXECUTION_SPEED = 0x1;
    private const uint PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION = 0x4;

    // המקום החמישי (4 מ-0) ב-PROCESS_INFORMATION_CLASS הוא ProcessPowerThrottling
    private const int ProcessPowerThrottling = 4;

    [DllImport("kernel32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool SetProcessInformation(
        IntPtr hProcess,
        int processInformationClass,
        ref PROCESS_POWER_THROTTLING_STATE processInformation,
        uint processInformationSize);

    [DllImport("kernel32.dll")]
    private static extern IntPtr GetCurrentProcess();

    /// <summary>מבטל גם את הנטייה לחיסכון בחשמל, וגם את ההתעלמות מבקשת רזולוציית הטיימר.</summary>
    public static void OptOutOfPowerThrottling()
    {
        var state = new PROCESS_POWER_THROTTLING_STATE
        {
            Version = PROCESS_POWER_THROTTLING_CURRENT_VERSION,
            ControlMask =
                PROCESS_POWER_THROTTLING_EXECUTION_SPEED |
                PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION,
            StateMask = 0,
        };

        if (!SetProcessInformation(
                GetCurrentProcess(),
                ProcessPowerThrottling,
                ref state,
                (uint)Marshal.SizeOf<PROCESS_POWER_THROTTLING_STATE>()))
        {
            throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
        }
    }
}

הקריאה מתבצעת פעם אחת עם PowerQos.OptOutOfPowerThrottling(); לפני התחלת הלולאה הרגישה לזמן. ל-handle של התהליך נדרשת הרשאת PROCESS_SET_INFORMATION, אבל ה-handle המדומה של התהליך העצמי שמחזיר GetCurrentProcess() לא בעייתי.

4.6. הקצאת CPU / מעבר בין ליבות / חום

בהקצאת CPU, לרוב עדיף להתחיל מציון רך הקרוב ל-soft affinity, “רוצים שהריצה תהיה בקבוצת הליבות הזו”, ולא ישר מנעילה לליבה ספציפית (hard affinity / CPU pinning).

סדר הצעדים בשיוך ליבותתרשים המראה שקודם מודדים, אחר כך מנסים SetThreadIdealProcessor או CPU Sets, ורק אם השיפור אינו מספיק שוקלים בסוף SetThreadAffinityMask, תוך בדיקה מקבילה של טמפרטורה, שעון והפעלה ארוכה.כןלאקודם מודדיםSetThreadIdealProcessor / CPU Setsהשתפר מספיק?עוצרים כאןבסוף שוקלים SetThreadAffinityMaskבאותו זמן בודקים גם טמפרטורה / שעון / הפעלה ארוכה

איור 17: הקצאת CPU מתחילה במדידה, אם ציון רך מספיק עוצרים שם, ונעילה לליבה ספציפית היא המוצא האחרון.

רשימת בדיקה

  • קודם מודדים, ורק אז נוגעים בהקצאת CPU
  • לא ננעלים ישר לליבה ספציפית
  • בודקים קודם את SetThreadIdealProcessor או CPU Sets
  • SetThreadAffinityMask נחשב מוצא אחרון
  • בהפעלה ארוכת טווח בודקים טמפרטורה, שעון וויסות עקב חום
  • בודקים מצב שקט או רעש נמוך במחשב נייד

הסדר הבטוח הוא הזרימה הזו.

  1. קודם מודדים
  2. במידת הצורך, ideal processor / CPU Sets
  3. אם עדיין נדרש שיפור, נעילה לליבה ספציפית

נעילה לליבה ספציפית נראית יעילה, אבל היא מקטינה את דרכי המילוט של מערכת ההפעלה, ולכן שימוש לא זהיר בה עלול להפוך את המצב לפחות גמיש.

4.7. הפרדת דרייבר / DPC / ISR / הפרעות חיצוניות

כשקורה “לפעמים רק המקסימום מתפוצץ” או “הממוצע טוב אבל p99.9 גרוע”, כדאי לחשוד גם בהפרעות חיצוניות מחוץ לקוד האפליקציה.

עץ ההחלטה כשמופיעה קפיצהתרשים המראה סדר בידוד כשמופיעה קפיצת late, miss או max: תחילה זמן העיבוד של האפליקציה, אחר כך DPC ו-ISR, אחר כך page fault, GC ועלות ראשונית, אחר כך סוללה, חיסכון בחשמל וחום, ולבסוף חפירה עם ETW, WPA או LatencyMon.כןלאכןלאכןלאכןלאהופיעה קפיצת late / miss / maxגם זמן העיבוד שלכם ארוך?קיצור hot path / הפחתת הקצאות / הסרת I/Oיש קפיצת DPC / ISR?בודקים USB / Wi-Fi / Bluetooth / GPU / אודיו / אחסון / ACPI / עדכון דרייבריש page fault / GC / עלות ראשונית?הקצאה מראש / חימום / הפחתת עומס בערמהיש השפעה של סוללה / חיסכון בחשמל / חום?חשמל AC / הגדרות צריכת חשמל / קירור / בדיקה ארוכת טווחחפירה עם ETW / WPA / LatencyMon

איור 18: סדר ההפרדה כשמופיעה קפיצה. חושדים לפי הסדר בעיבוד שלכם עצמו, ב-DPC/ISR, בזיכרון, ובצריכת חשמל וחום.

רשימת בדיקה

  • בודקים דרייברים סביב Wi-Fi / Bluetooth / USB / אחסון / GPU / אודיו
  • מכבים סנכרון ענן, בניית אינדקס, ועדכון אוטומטי מיותרים ומשווים
  • בודקים גם אם זה נשבר בהקטנה, או אם זה נשבר כשהמסך כבוי
  • בודקים מגמות DPC / ISR עם LatencyMon או ETW
  • מבדילים בין “העיבוד שלנו כבד” לבין “מישהו מבחוץ עוצר אותנו”

5. מדידה והערכה

5.1. מה מתעדים

אלה הדברים שכדאי לקחת לפחות.

  • זמן מתוכנן של המחזור
  • זמן התחלה בפועל
  • זמן סיום בפועל
  • lateness (כמה איחור ביחס להתחלה המתוכננת)
  • זמן ריצה
  • מספר החמצות דדליין
  • מספר החמצות דדליין ברצף
  • עומק תור
  • מספר drop
  • ניצול CPU
  • הטיה בין ליבות
  • קפיצות DPC / ISR
  • page fault
  • תנודות טמפרטורה / שעון

גם אם מסתכלים רק על הממוצע, קשה לתפוס את המהות. מה שגורם לבעיות בייצור הוא הקפיצות הגדולות שמופיעות מדי פעם.

5.2. איך קוראים p99 / p99.9 / מקסימום

מדדים כמו p99 נועדו לראות את הזנב האיטי. בממוצע בלבד, ההשהיה הגדולה שמופיעה מדי פעם מוסתרת.

מדד משמעות תמונה כשמודדים 10,000 פעמים
ממוצע ערך ממוצע כללי קל שהקפיצה תיטמע בו
p50 הערך האמצעי קרוב לתחושה היומיומית
p95 הגבול שבו מתחילים להיראות 5% האיטיים הגבול לאחר הוצאת 500 המדידות האיטיות
p99 הגבול שבו מתחיל להיראות 1% האיטי הגבול לאחר הוצאת 100 המדידות האיטיות
p99.9 הגבול שבו מתחיל להיראות 0.1% האיטי הגבול לאחר הוצאת 10 המדידות האיטיות
מקסימום הערך הגרוע ביותר המדידה האיטית ביותר מכולן

לדוגמה, נניח שהתקבל הרצף הבא (דוגמה להסבר איך קוראים את המדדים, לא ערכים נמדדים בפועל מציוד מסוים).

  • ממוצע: 0.8ms
  • p99: 1.2ms
  • p99.9: 3.5ms
  • מקסימום: 28ms

זהו מצב שבו בדרך כלל מהיר, אבל מדי פעם יש קפיצה גדולה. ב-Windows רגיל, הבעיה האמיתית מופיעה בדרך כלל בזנב הזה שבין p99 למקסימום.

ההבדל בין הממוצע לבין המדדים בצד הזנבתרשים המראה שכשמסתכלים רק על הממוצע, ההשהיה הגדולה שמופיעה מדי פעם מוסתרת, בעוד שכשמסתכלים על p99, p99.9 ומקסימום, נראה הזנב האיטי, וב-Windows רגיל הבעיה האמיתית מופיעה שם.מסתכלים רק על הממוצעההשהיה הגדולה שמופיעה מדי פעם מוסתרתמסתכלים על p99, p99.9 ומקסימוםנראה הזנב האיטיהבעיה האמיתית מופיעה בין p99 למקסימום

איור 19: גם אם הממוצע טוב, אם המקסימום קופץ, זה מצב “בדרך כלל מהיר אבל קופץ מדי פעם”. תופסים את זה עם מדדי הזנב.

נציין שהמאמר הזה אינו מציג ערכים מספריים להשפעת המבנה המומלץ. השהיה ו-jitter משתנים בקלות לפי CPU, דרייבר, תוכנה נלווית, הגדרות צריכת חשמל ואופן העמסת העומס, ולכן ערכים ממחשב של מישהו אחר לא משמשים כשלעצמם הוכחה עבור הסביבה שלכם. במקום זאת, נציג נוהל ליצירת אותה טבלה עבור הסביבה שלכם.

הנוהל המינימלי להוצאת p99 בסביבה שלכם

  1. ב-hot path מתעדים בלבד. לוקחים lateness וזמן ריצה עם Stopwatch.GetTimestamp() (ב-C++:‏ QueryPerformanceCounter), וכותבים למערך שהוקצה מראש. אסור לחשב כאן ממוצע או מיון
  2. עוצרים את המדידה, ורק אז מרכזים. ממיינים, ומוציאים את הערך במיקום האחוזון
  3. חוזרים על אותו נוהל בשינוי תנאים: לפני/אחרי חימום, AC/סוללה, UI קדמי/ממוזער, עם/בלי עומס מתהליכים אחרים (5.4)
  4. בכל שינוי בודד, לוקחים מדידה חדשה באותם תנאים ומשווים
// C# / .NET 8. הריכוז מתבצע אחרי שעוצרים את המדידה
using System.Diagnostics;

// ב-hot path רק כותבים למערך (הקצאה אפס)
long[] latenessTicks = new long[100_000];
int count = 0;

// דוגמה: בתוך הלולאה המחזורית
// latenessTicks[count++] = Stopwatch.GetTimestamp() - scheduledTimestamp;

static double PercentileMs(long[] ticks, int count, double percentile)
{
    long[] sorted = ticks.AsSpan(0, count).ToArray();
    Array.Sort(sorted);

    int index = (int)Math.Ceiling(percentile / 100.0 * count) - 1;
    index = Math.Clamp(index, 0, count - 1);

    return sorted[index] * 1000.0 / Stopwatch.Frequency;
}

// שימוש
// Console.WriteLine($"p50={PercentileMs(latenessTicks, count, 50):F3}ms");
// Console.WriteLine($"p99={PercentileMs(latenessTicks, count, 99):F3}ms");
// Console.WriteLine($"p99.9={PercentileMs(latenessTicks, count, 99.9):F3}ms");
// Console.WriteLine($"max={PercentileMs(latenessTicks, count, 100):F3}ms");

Stopwatch.Frequency הוא מספר המונים לשנייה, ולכן אם מחלקים ומכפילים ב-1000 מקבלים מילישניות. אם מספר המדגמים קטן מדי, ל-p99.9 אין משמעות. אם רוצים לדבר על p99.9, יש לאסוף לפחות 10,000 מדגמים (רצוי 100,000).

5.3. באילו כלים מסתכלים

מערך הכלים כבר קבוע בגדול.

  • מדידה פנימית באפליקציה קודם לוקחים בעצמכם period / lateness / execution time / queue depth / drop
  • ETW / WPR / WPA חופרים ב-CPU,‏ context switch,‏ DPC / ISR ו-page fault
  • LatencyMon מזהים חשוד בתנודה שמקורה בדרייבר
  • ניטור טמפרטורה / שעון בודקים את השפעת החום
מה מודדים ואיך מחליטיםתרשים המראה שמדידה פנימית באפליקציה נותנת התפלגות אחוזונים ומדדי miss, drop ועומק תור, ש-ETW, WPR ו-WPA מגלים context switch, DPC, ISR ו-page fault, ושניטור טמפרטורה ושעון מצטרף אליהם כדי לקבוע סדר עדיפות לשיפור.מדידה פנימית באפליקציהp50 / p95 / p99 / p99.9 / maxmiss / drop / עומק תורETW / WPR / WPAcontext switch / DPC / ISR / page faultניטור טמפרטורה / שעוןקובעים סדר עדיפות לשיפור

איור 20: משווים בין תוצאות המדידה הפנימית, ETW וניטור הטמפרטורה, וקובעים לפיהן סדר עדיפות לשיפור.

עד להגעה ל-WPA זה קצת מייגע, אבל זה יעיל מאוד להבחין בין האם DPC / ISR הם הגורם, או שהעיבוד שלכם עצמו פשוט כבד.

איך משיגים ואופן שימוש מינימלי

כלי איך משיגים הנוהל המינימלי
WPR / WPA (Windows Performance Toolkit) מותקן כשבוחרים “Windows Performance Toolkit” בהתקנת Windows ADK (Windows Assessment and Deployment Kit). המיקום המובנה: C:\Program Files (x86)\Windows Kits\10\Windows Performance Toolkit פותחים שורת פקודה כמנהל, (1) מתחילים הקלטה עם wpr -start CPU, (2) מריצים את העיבוד הבעייתי כמה עשרות שניות, (3) שומרים עם wpr -stop trace.etl "בדיקת השהיה מחזורית". אחר כך פותחים את trace.etl ב-WPA. שמות הפרופילים הזמינים ניתן לבדוק עם wpr -profiles
LatencyMon מורידים מהאתר של Resplendence Software. גרסת Home Edition לשימוש אישי היא בחינם מפעילים, מתחילים מדידה, ומריצים את העיבוד הרלוונטי כמה דקות. מתקבל ריכוז של השהיית הטיימר המקסימלית של הקרנל, וגם זמני ISR / DPC לפי דרייבר, ו-hard pagefault, כך שרושמים את הדרייברים שזמן הריצה שלהם בולט

מה שמסתכלים עליו קודם ב-WPA, מבין הגרפים הקשורים ל-CPU, הם זמן הריצה של DPC / ISR ו-context switch. מוצג “איזה דרייבר החזיק את ה-CPU בזמן שהת’רד שלכם לא יכול היה לרוץ”, ומשווים את זה לזמני ההשהיה שתועדו ב-5.1.

נציין שהמאמר הזה לא כולל צילומי מסך. המראה משתנה הרבה בין גרסאות, ולכן אנא בצעו את השלבים למעלה ובדקו במסך האמיתי.

5.4. שיטת הבדיקה

הבדיקה לא מספיקה בסביבת benchmark שקטה בלבד. כדאי לפחות לבדוק בנפרד את התנאים האלה.

  • לפני החימום, מיד אחרי ההפעלה
  • אחרי החימום
  • הפעלה ארוכה ורציפה
  • UI קדמי
  • מצב קרוב ל-UI ממוזער / מוסתר
  • חשמל AC
  • הפעלה על סוללה
  • מצב עם עומס על רשת או דיסק

הערכה רק בסביבת benchmark נוטה לפספס בעיות שמופיעות בתפעול בפועל. מכיוון של-Windows רגיל נוטה להיות התנהגות שנגררת אחרי “אופן השימוש”, חשוב לבדוק תוך התקרבות לתנאים בפועל.

החשיבה שמאחורי בדיקה בתנאים נפרדיםתרשים המראה שהערכה רק בסביבת benchmark שקטה מפספסת בעיות מהתפעול בפועל, ולכן בודקים בנפרד תנאים כמו לפני/אחרי חימום והפעלה ארוכה, UI קדמי מול ממוזער, וחשמל AC מול סוללה ועומס.הערכה רק בסביבת benchmark שקטהמפספסים בעיות מהתפעול בפועלבודקים בתנאים נפרדיםחימום והפעלה ארוכהUI קדמי וממוזערתנאי חיבור לחשמל ועומס

איור 21: ל-Windows רגיל יש התנהגות שנגררת אחרי אופן השימוש. מעריכים תוך התקרבות לתנאי השימוש בפועל.

6. חלוקה גסה בין השימושים

הטבלה המהירה לפי טווח מחזור הועברה לסעיף 1.1 כדי שתוכלו לחזור אליה תוך כדי קריאה. כאן נשלים שתי החלטות שהטבלה הזו לבדה לא קובעת.

מתי מחליטים ש”אי אפשר עם Windows רגיל”

כשמופיעה דרישה לשמור על פחות מ-1ms לאורך זמן ובעומס גבוה, לרוב מהיר יותר להחליט להעביר החוצה רק את החלק הרגיש לזמן, מאשר להמשיך לכוונן. מועמדי היעד הם קושחת ההתקן, בקר ייעודי, FPGA,‏ RTOS. חומר ההחלטה צריך להיות לא סובייקטיבי אלא המספרים מפרק 5.

  • למרות שאי אפשר לקצר עוד את ה-hot path, p99.9 והמקסימום ממשיכים לחרוג מהדרישה
  • הגורם הוא הפרעה חיצונית (DPC / ISR, דרייבר, תהליך אחר), ואמצעים בצד האפליקציה לא מספיקים (מאושר לפי ההפרדה בסעיף 4.7)
  • הדרישה היא לא “לא נשברים גם כשמפספסים מדי פעם” אלא “אסור לפספס אפילו פעם אחת”

כשרוצים לגור עם הכול בתהליך אחד

כשמחזיקים GUI, יומן, תקשורת ו-DB באותה לולאה של אותו תהליך, נטיית העניינים בשלב המאוחר שוברת את הדדליין של השלב המוקדם. המתנה ל-flush של קובץ, חיבור מחדש ל-DB, וציור מחדש של UI, כולם יכולים להתארך בסדר גודל של עשרות מילישניות. הרחבת ההפרדה בין fast path ל-slow path (4.2) עד לרמת התהליך, ולא רק הת’רד, היא גם אפשרות. כשמפרידים לתהליכים, גם אם השלב המאוחר נתקע, המחזור של השלב המוקדם ממשיך לרוץ.

השוואה בין מבנה משותף להפרדת תהליכיםתרשים המראה שכשמחזיקים GUI, יומן, תקשורת ו-DB באותה לולאה של אותו תהליך, נטיית העניינים בשלב המאוחר שוברת את הדדליין של השלב המוקדם, ואילו הרחבת הפרדת fast path/slow path לרמת התהליך שומרת על המחזור המוקדם גם כשהשלב המאוחר נתקע.מחזיקים הכול בתהליך אחד ולולאה אחתנטיית השלב המאוחר שוברת את הדדליין המוקדםהמתנה ל-flush, חיבור מחדש, ציור מחדשמפרידים בין השלב המוקדם למאוחר לפי תהליךגם אם השלב המאוחר נתקע, המוקדם ממשיך לרוץ

איור 22: אם באמת נדרש לגור יחד, יש אפשרות להרחיב את הפרדת fast path / slow path עד לרמת התהליך.

7. סיכום

יש שתי הנחות שכדאי לזכור.

  • מה שרודפים אחריו ב-Windows רגיל הוא לא הבטחת hard real-time, אלא מבנה של soft real-time שמקטין השהיה ו-jitter ולא נשבר גם כשמפספסים דדליין
  • מה שהכי משפיע הוא סידור ה-hot path, יותר מכיוונון עדיפות

מבחינת מימוש, מה שיעיל הוא בערך זה.

  • מפרידים בין fast path ל-slow path
  • תור באורך קבוע, ומדיניות מוחלטת מראש למקרה שהוא עולה על גדותיו
  • מודדים עם QPC, וממתינים עם event / waitable timer (טיימר בר-המתנה)
  • נמנעים ב-hot path מהקצאה, I/O חוסם, ונעילות כבדות

מבחינת תפעול, מה שיעיל הוא בערך זה.

  • מפעילים על חשמל AC
  • מפרידים הגדרות צריכת חשמל לייצור
  • מפחיתים עומס רקע מיותר
  • מעריכים לפי p99 / p99.9 / מקסימום ומספר החמצות

זמן אמת רך ב-Windows רגיל לא נקבע רק על ידי הגדרת עדיפות, אלא אם מסדרים בנפרד תכנון, מימוש, הגדרות צריכת חשמל, מדידה ותפעול, אפשר להגיע למערכת יציבה למדי.

8. מקורות

מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.

העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.

שאלות נפוצות

שאלות נפוצות בפניות בנושא המאמר.

האם אפשר לבצע עיבוד בזמן אמת ב-Windows?
אי אפשר להבטיח hard real-time (אפס החמצות דדליין), אבל אם מסדרים היטב תכנון, מימוש, מדידה ותפעול, אפשר להגיע למצב מעשי למדי כ-soft real-time. עיבוד מחזורי של כמה מילישניות עד עשרות מילישניות, הפעלה מבוססת מאגר לאודיו/וידאו, איסוף מחיישנים ולולאות בקרה - כל אלה מעשיים גם ב-Windows 10/11 רגיל. לעומת זאת, אם נדרשת הבטחה של אפס החמצות דדליין או יציבות ארוכת טווח מתחת למאות מיקרו-שניות, כדאי לשקול להעביר את זה ל-RTOS, בקר ייעודי, FPGA או עיבוד בצד ההתקן.
למה אסור להשתמש ב-Sleep(1) בעיבוד מחזורי?
כי Sleep(1) לא נותן "מחזור של 1ms" אלא התנהגות של "להמתין בערך 1ms ומעלה, ואז להוסיף את זמן העיבוד", והחריגה בהמתנה מצטברת כמו שהיא. לולאה מחזורית יציבה יותר כשהיא רצה לפי דדליין מוחלט בעזרת next += period, ההמתנה מעדיפה אירוע התקן או waitable timer ברזולוציה גבוהה, וכיוונון עדין אחרון בלבד מוגבל ל-busy-spin קצר מאוד. את timeBeginPeriod משתמשים רק בזמן הנדרש, ומחזירים אחר כך.
מה הכי יעיל להפחתת השהיה ו-jitter?
לא העלאת עדיפות, אלא לקצר את ה-hot path, להפוך אותו לאורך קבוע ולא-חוסם. מפרידים בין fast path (איסוף ובקרה) ל-slow path (שמירה, תקשורת, UI) ומחברים אותם בתור באורך קבוע, ונמנעים ב-hot path מכתיבה לקובץ, שליחה ברשת, יומן כבד, והקצאת זיכרון בכל פעם. בצד התפעול, יעילים חיבור לחשמל, בדיקת מצב צריכת החשמל, בדיקת EcoQoS, וסידור עומס הרקע.
באילו מדדים כדאי להעריך את היציבות של עיבוד מחזורי?
לא רק בממוצע, אלא ב-p99,‏ p99.9, מקסימום ומספר החמצות דדליין. לדוגמה, גם עם ממוצע של 0.8ms אבל מקסימום של 28ms, המצב הוא בדרך כלל מהיר אבל עם קפיצות גדולות מדי פעם, וב-Windows רגיל הבעיה האמיתית מופיעה בדיוק בזנב הזה שבין p99 למקסימום. יחד עם זה, כדאי לתעד גם קפיצות DPC/ISR, page fault, עומק תור, ותנודות טמפרטורה/שעון, ולהעריך בנפרד תנאים כמו לפני/אחרי חימום, הפעלה ארוכת טווח, מצב ממוזער, והפעלה על סוללה.

פרופיל הכותב

עמוד היכרות עם כותב המאמר.

Go Komura

מנהל KomuraSoft LLC

מתמחה בפיתוח תוכנה עבור Windows, ייעוץ טכני וחקירת תקלות, בעיקר בפרויקטים עם מערכות קיימות ובאגים שקשה לשחזר.

קישורים ציבוריים

חזרה לבלוג