מדריך מעשי להשגת מרב זמן אמת רך ב-Windows רגיל
· עודכן בתאריך: · Go Komura · פיתוח 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. טבלה מהירה לפי טווח מחזור (מאיפה לקרוא)
- מה זה “זמן אמת רך” ב-Windows רגיל
- 2.1. מה נקרא “Windows רגיל” במאמר הזה
- 2.2. מה אפשר לעשות, ומאיפה זה נהיה קשה
- 2.3. מונחים קודם, במשפט אחד
- הגורמים העיקריים להשהיה ו-jitter
- 3.1. תזמון ועדיפות
- 3.2. DPC / ISR ודרייברים
- 3.3. page fault וזיכרון
- 3.4. רזולוציית טיימר וניהול צריכת חשמל
- 3.5. מעבר בין ליבות וחום
- רשימת בדיקה מעשית להפחתת השהיה ב-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.1. מה מתעדים
- 5.2. איך קוראים p99 / p99.9 / מקסימום
- 5.3. באילו כלים מסתכלים
- 5.4. שיטת הבדיקה
- חלוקה גסה בין השימושים
- סיכום
- מקורות
מפת הידע של המאמר
כדי לשאוף ל-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.
flowchart LR
accTitle: מפת הידע של זמן אמת רך ב-Windows רגיל
accDescr: תרשים המראה את הקשר בין waitable timer ו-MMCSS שתומכים בזמן אמת רך, את הקשר בין Power Throttling ל-EcoQoS, את המסלול שבו DPC/ISR ו-page fault יוצרים jitter, את השלבים מ-CPU Sets ועד נעילת CPU, ואת הקשר בין WPR/WPA ל-LatencyMon ככלי מדידה.
soft_realtime["זמן אמת רך"]
qpc["QPC(QueryPerformanceCounter)"]
waitable_timer["טיימר בר-המתנה (waitable timer)"]
timebeginperiod["timeBeginPeriod (בקשת רזולוציית טיימר)"]
power_throttling["Power Throttling (ויסות צריכת חשמל)"]
ecoqos["EcoQoS"]
jitter["jitter (פיזור זמנים)"]
mmcss["MMCSS(Multimedia Class Scheduler Service)"]
deadline_miss["החמצת דדליין"]
thread_priority_class["עדיפות ת'רד ומחלקת עדיפות"]
cpu_sets["CPU Sets"]
cpu_affinity["זיקת מעבד (CPU affinity)"]
thermal_throttling["ויסות עקב חום (thermal throttling)"]
dpc_isr["DPC/ISR"]
page_fault["page fault (שגיאת דף)"]
wpr_wpa["WPR/WPA(Windows Performance Recorder/Analyzer)"]
latencymon["LatencyMon"]
soft_realtime -.->|"משתמש ב"| qpc
soft_realtime -.->|"משתמש ב"| waitable_timer
soft_realtime -.->|"משתמש ב"| timebeginperiod
power_throttling -.->|"אינו מתיישב עם"| timebeginperiod
soft_realtime -.->|"משתמש ב"| power_throttling
power_throttling -.->|"מונע"| ecoqos
ecoqos -.->|"עלול לגרום ל"| jitter
soft_realtime -.->|"משתמש ב"| mmcss
mmcss -->|"מצמצם"| jitter
mmcss -->|"מצמצם"| deadline_miss
thread_priority_class -.->|"עלול לגרום ל"| jitter
thread_priority_class -->|"שימוש לא מומלץ ל"| soft_realtime
soft_realtime -.->|"משתמש ב"| cpu_sets
cpu_sets -->|"צריך לקדום ל"| cpu_affinity
thermal_throttling -.->|"עלול לגרום ל"| jitter
dpc_isr -.->|"עלול לגרום ל"| jitter
page_fault -.->|"עלול לגרום ל"| jitter
dpc_isr -->|"נבדק באמצעות"| wpr_wpa
dpc_isr -->|"נבדק באמצעות"| latencymon
page_fault -->|"נבדק באמצעות"| wpr_wpa
בתרשים, קו מלא מציין קשר שמתקיים תמיד וקו מקווקו מציין קשר מותנה (תנאי ההתקיימות מפורטים בהסבר של כל קשר בעמוד המפורט). רשימת כל הקשרים (סך הכול 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 רגיל הקטנת סיבות ההשהיה בתכנון עוזרת יותר מהעלאת עדיפות. עדיפות והגדרות צריכת חשמל חשובות, אבל הן לבדן לא יוצרות יציבות.
flowchart TB
accTitle: יציבות נוצרת מתכנון, לא מעדיפות
accDescr: תרשים המראה שקיצור ה-hot path והפיכתו לאורך קבוע ולא-חוסם מקטינה בתכנון את סיבות ההשהיה ומובילה למבנה שקשה לו להתעכב ולא נשבר, בעוד עדיפות והגדרות צריכת חשמל חשובות אך לא מספיקות לבדן.
hot["hot path קצר ובאורך קבוע"] --> less["סיבות ההשהיה קטנות בתכנון"]
less --> goal["מבנה שקשה לו להתעכב ולא נשבר"]
prio["כיוונון עדיפות והגדרות צריכת חשמל"] -.-> goal
prio -.-> note["חשוב אך לא מספיק לבדו"]
איור 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 רגיל”.
flowchart LR
accTitle: היקף המאמר
accDescr: תרשים המראה שהמאמר עוסק באפליקציית user-mode על מחשב Windows רגיל שמכוונת לזמן אמת רך — השהיה נמוכה, פחות jitter וניטור החמצות דדליין — ושדרישה לאפס החמצות מחייבת RTOS, בקר ייעודי, FPGA או עיבוד בצד ההתקן.
A["מחשב Windows 10 / 11 רגיל"] --> B["אפליקציית user-mode"]
B --> C["שואפים ל-soft real-time"]
C --> D["מנמיכים השהיה"]
C --> E["מקטינים jitter"]
C --> F["מנטרים החמצות דדליין ולא נשברים"]
G["רוצים להבטיח אפס החמצות דדליין"] -.-> H["RTOS / בקר ייעודי / FPGA / עיבוד בצד ההתקן"]
איור 2: באפליקציית user-mode ב-Windows רגיל שואפים ל-soft real-time; הבטחת אפס החמצות דדליין שייכת לתחום ה-RTOS וכדומה.
2.2. מה אפשר לעשות, ומאיפה זה נהיה קשה
גם ב-Windows רגיל, אפשר ליצור באופן מעשי מצב של “קשה להתעכב” בעיבודים כמו אלה.
- עיבוד מחזורי של כמה מילישניות עד עשרות מילישניות
- הפעלה מבוססת מאגר לאודיו / וידאו
- איסוף מחיישנים ולולאות בקרה
- עיבוד מחזורי קבוע בסגנון PLC תוכנתי
- צינור עיבוד בהשהיה נמוכה שרץ בת’רד נפרד מה-UI
עם זאת, “אפשר” כאן אינו אומר שאפשר לאפס לגמרי קפיצות השהיה מדי פעם, אלא המטרה היא בעיקר המצב הבא.
- הנמכת ההשהיה הרגילה
- הקטנת ה-jitter
- לא להישבר גם כשמפספסים דדליין מדי פעם
- היכולת לצפות בעובדה שפספסנו
לעומת זאת, כשהדרישה היא כזו, קשה מאוד לספק אותה עם user-mode ב-Windows רגיל בלבד.
- רוצים להבטיח אפס החמצות דדליין
- רוצים לשמור בעקביות על פחות ממאות מיקרו-שניות לאורך זמן
- רוצים לגור יחד עם GUI כבד, רשת ואחסון
- רוצים לעשות זאת תוך הפעלה על סוללה או עדיפות לחיסכון בחשמל
- אסור שגם קפיצות שמקורן בדרייבר או בהתקן יופיעו
במקרים כאלה, בטוח יותר לשקול להעביר רק את החלק שבאמת דורש דיוק זמנים חמור לקושחת ההתקן, לבקר ייעודי, ל-FPGA או ל-RTOS.
flowchart TB
accTitle: המצב שרודפים אחריו ב-soft real-time וההחלטה מתי להעביר החוצה
accDescr: תרשים המראה שב-Windows רגיל שואפים להנמיך את ההשהיה הרגילה, להקטין jitter, ולא להישבר תוך יכולת ניטור כשמפספסים דדליין מדי פעם, ואילו דרישה חמורה כמו אפס החמצות דדליין שווה להעביר לחלק שרק בו נדרש דיוק זמנים.
aim["המצב שרודפים אחריו ב-soft real-time"] --> s1["הנמכת ההשהיה הרגילה"]
aim --> s2["הקטנת ה-jitter"]
aim --> s3["לא נשברים גם כשמפספסים, וניתן לנטר"]
strict["דרישה של אפס החמצות דדליין"] -.-> out["מעבירים ל-התקן או 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 רגיל, מגיעה בדרך כלל לאחד מהענפים בתרשים הזה.
flowchart TD
accTitle: גורמים לעיכוב בעיבוד מחזורי
accDescr: תרשים המונה חמישה כיווני חקירה כשעיבוד מחזורי מתעכב: תזמון ועדיפות, DPC ו-ISR ודרייברים, page fault וזיכרון, רזולוציית טיימר וניהול צריכת חשמל, ומעבר בין ליבות וחום.
Late["עיבוד מחזורי מתעכב"] --> S["תזמון / עדיפות"]
Late --> D["DPC / ISR / דרייבר"]
Late --> M["page fault / זיכרון"]
Late --> T["רזולוציית טיימר / ניהול צריכת חשמל"]
Late --> C["מעבר בין ליבות / חום"]
איור 4: הסיבות לעיכוב עיבוד מחזורי מגיעות לחמש מערכות - תזמון, DPC/ISR, זיכרון, טיימר וצריכת חשמל, ומעבר בין ליבות וחום.
3.1. תזמון ועדיפות
סדר הריצה של ת’רדים ב-Windows נקבע לפי עדיפות. באותה עדיפות רצים לפי round-robin, וכשת’רד בעדיפות גבוהה יותר הופך לניתן להרצה, ת’רד בעדיפות נמוכה יותר נדחק הצידה.
כלומר, גם אם כותבים ת’רד מחזורי ברצינות, יכולים באופן שגרתי לרוץ לפניו:
- ת’רד אחר
- תהליך אחר
- עיבוד פנימי של מערכת ההפעלה
- מוצר אבטחה
- עיבוד עזר של התקן
- סנכרון ברקע
flowchart TB
accTitle: דחיקה על ידי תזמון לפי עדיפות
accDescr: תרשים המראה שת'רדים באותה עדיפות רצים בסבב round-robin, וכשת'רד בעדיפות גבוהה יותר הופך לניתן להרצה, ת'רד בעדיפות נמוכה נדחק הצידה, ולכן זה שגרתי שתהליך אחר או עיבוד פנימי של מערכת ההפעלה ירוצו לפני ת'רד מחזורי.
same["ת'רדים באותה עדיפות"] --> rr["רצים בסבב round-robin"]
high["עדיפות גבוהה יותר הופכת לניתנת להרצה"] --> push["עדיפות נמוכה נדחקת הצידה"]
push -.-> who["למשל תהליך אחר או עיבוד פנימי של מערכת ההפעלה"]
איור 5: גם כשכותבים ת’רד מחזורי ברצינות, זה שגרתי שעבודה בעדיפות גבוהה יותר תרוץ לפניו.
3.2. DPC / ISR ודרייברים
זו נקודה חשובה מאוד. גם אם מסדרים את העדיפות בצד האפליקציה, אם DPC (Deferred Procedure Call) או ISR (Interrupt Service Routine) ארוכים, ת’רד ה-user-mode לא יכול לרוץ באותו זמן.
ההתקנים והדרייברים שנוטים לגרום לזה הם בערך אלה.
- USB
- Wi-Fi / Bluetooth
- אחסון
- אודיו
- GPU
- ACPI וסביבת צריכת החשמל
גם כשהקוד של האפליקציה לא רע, אפשר להיעצר בגלל צרכי הדרייבר או החומרה. כאן, אם חושבים “אם מעלים עוד יותר את עדיפות האפליקציה, ננצח” - בדרך כלל נכשלים בכאב.
flowchart TB
accTitle: איך DPC ו-ISR עוצרים את ה-user-mode
accDescr: תרשים המראה שכשהתקנים כמו USB, Wi-Fi ו-GPU מפעילים ISR או DPC ארוכים בצד הקרנל, ת'רד ה-user-mode לא יכול לרוץ באותו זמן, וגם העלאת עדיפות האפליקציה לא מנצחת את זה.
dev["התקנים כמו USB, Wi-Fi, GPU"] --> kern["ISR או DPC רצים בצד הקרנל"]
kern --> block["באותו זמן ה-user-mode לא יכול לרוץ"]
block -.-> note["גם העלאת עדיפות לא מנצחת"]
איור 6: גם כשהקוד של האפליקציה לא רע, ה-user-mode נעצר בגלל צרכי הדרייבר או החומרה.
3.3. page fault וזיכרון
כשמתרחש page fault (העמוד הדרוש לא נמצא בזיכרון, ויוצאים להביא אותו) ב-hot path, ההשהיה גדלה בבת אחת.
נציין תבניות שכדאי במיוחד להימנע מהן.
- commit של עמוד בגישה הראשונה
- טעינה מושהית (lazy load)
- page-in של קובץ ממופה לזיכרון
- הקצאה דינמית מעבר לנדרש
- אובייקטים גדולים או ערמה מפוצלת
בגוף העיבוד המחזורי, נכון להקצות מראש את הזיכרון הדרוש ולגעת בו פעם אחת בזמן ההפעלה.
flowchart TB
accTitle: השהיה הנגרמת מ-page fault ב-hot path
accDescr: תרשים המראה שאם העמוד שנוגעים בו ב-hot path לא נמצא בזיכרון, יוצאים להביא אותו ב-page fault וההשהיה גדלה בבת אחת, ולכן עדיף להקצות מראש ולגעת פעם אחת בזמן ההפעלה.
touch["נוגעים בזיכרון ב-hot path"] --> q{"העמוד נמצא בזיכרון?"}
q -->|"כן"| ok["ממשיכים כרגיל"]
q -->|"לא"| pf["יוצאים להביא אותו ב-page fault"]
pf --> spike["ההשהיה גדלה בבת אחת"]
pf -.-> fix["הקצאה מראש ונגיעה פעם אחת בהפעלה"]
איור 7: ההשהיה קופצת לפי אם מתרחש page fault. הפתרון הוא הקצאה מראש וחימום בזמן ההפעלה.
3.4. רזולוציית טיימר וניהול צריכת חשמל
“רוצה להריץ כל 1ms, אז Sleep(1)” - כמעט תמיד לא עובד.
דיוק ההמתנה ב-Windows מושפע מרזולוציית הטיימר, מהתזמון, וממצב צריכת החשמל.
מעבר לזה, הגדרה שמעלה את רזולוציית הטיימר משפרת מעט את דיוק ההמתנה, אבל יש לה תופעות לוואי על צריכת החשמל והתנהגות המערכת כולה - נקודה שאי אפשר להתעלם ממנה.
3.5. מעבר בין ליבות וחום
כשת’רד עובר בין ליבות, נוצר חימום מחדש של המטמון. זה עצמו מטופל לרוב היטב על ידי מערכת ההפעלה, אבל בסביבה עמוסה זה הופך לגורם תנודה.
מעבר לזה, בהפעלה ארוכת טווח גם החום אי אפשר להתעלם ממנו. כשמופעל ויסות עקב חום (thermal throttling), המחזור שהיה יציב עד אז יכול להישבר.
flowchart TB
accTitle: המסלול שבו מעבר בין ליבות וחום שוברים את המחזור
accDescr: תרשים המראה שמעבר ת'רד בין ליבות גורם לחימום מחדש של המטמון שהופך לגורם תנודה בסביבה עמוסה, ובהפעלה ארוכת טווח הצטברות חום מפעילה ויסות עקב חום ששובר את המחזור שהיה יציב.
move["ת'רד עובר בין ליבות"] --> cache["חימום מחדש של המטמון"]
cache --> jitter["בסביבה עמוסה - גורם תנודה"]
heat["בהפעלה ארוכה מצטבר חום"] --> thr["ויסות עקב חום"]
thr --> broke["המחזור היציב נשבר"]
איור 8: מעבר בין ליבות פוגע ביציבות דרך jitter, וחום פוגע בה דרך ויסות, שניהם בהפעלה ארוכת טווח.
4. רשימת בדיקה מעשית להפחתת השהיה ב-Windows רגיל
מכאן זה החלק המעשי. לגורמים שראינו בסעיף הקודם, נרכז בצורת רשימת בדיקה מה בודקים, ממה נמנעים, ומה מחליטים מראש ב-Windows רגיל.
4.1. לולאה מחזורית ושיטת המתנה
קודם כול, אנטי-דפוס טיפוסי הוא זה.
while (running)
{
Sleep(1);
Step();
}
זו לא לולאה של “מחזור 1ms”, אלא לולאה שבה ממתינים בערך 1ms ומעלה, ומעל זה מוסיפים את זמן הריצה של Step().
בנוסף, חריגת ההמתנה מצטברת כמו שהיא.
flowchart LR
accTitle: המתנה יחסית מול דדליין מוחלט
accDescr: תרשים המשווה בין לולאה מבוססת Sleep יחסי, שבה שגיאת ההמתנה וזמן הריצה מצטברים בהדרגה, לבין לולאה מבוססת דדליין מוחלט שמקדמת את הזמן הבא בקבוע ולכן קשה לצבור בה drift.
subgraph Bad["מבוסס זמן יחסי"]
B1["Sleep(1)"] --> B2["Step()"]
B2 --> B1
end
B2 --> B3["שגיאת ההמתנה וזמן הריצה מצטברים בהדרגה"]
subgraph Good["מבוסס דדליין מוחלט"]
G1["next += period"] --> G2["WaitUntil(next - margin)"]
G2 --> G3["spin קצר במידת הצורך"]
G3 --> G4["FastStep()"]
G4 --> G1
end
G4 --> G5["קשה לצבור 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.
flowchart LR
accTitle: הפרדה בין fast path ל-slow path
accDescr: תרשים המראה שאירוע מההתקן מטופל ב-fast path עם עבודה מינימלית, עובר דרך תור באורך קבוע אל slow path שמבצע שמירה, שליחה, UI וסיכום, ושה-fast path מתעד גם lateness, miss ועומק תור.
Input["התקן / אירוע איסוף"] --> Fast["fast path: איסוף, בקרה, העתקה מינימלית"]
Fast --> Queue["תור באורך קבוע"]
Queue --> Slow["slow path: שמירה, שליחה, UI, סיכום"]
Fast --> Metrics["תיעוד lateness / miss / עומק תור"]
Metrics --> Slow
איור 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 וריכוז יומן מופרדים לתדירות נמוכה
כשהתור מתמלא, בטוח יותר לא להשאיר את המדיניות מעורפלת.
flowchart TD
accTitle: מדיניות כשהתור מתמלא
accDescr: תרשים המראה שלוש מדיניות אפשריות כשהתור מלא, לפי מה שחשוב לשמור: מחיקת ישן ושמירת העדכני, התראה ועצירה או בקרת המקור, או הפלת ישן עם תיעוד מספר ה-drop בלבד.
Overflow["התור מלא"] --> Policy{"מה שומרים?"}
Policy -->|"הערך העדכני חשוב"| Latest["מוחקים ישן ומשאירים עדכני"]
Policy -->|"כל הרשומות חשובות"| All["התראה / עצירה / בקרת המקור"]
Policy -->|"לצורכי יומן"| Log["מפילים ישן, ומתעדים רק את מספר ה-drop"]
איור 11: מה שומרים כשהתור מתמלא מוחלט מראש לפי מטרת השימוש.
4.3. עדיפות / MMCSS / background mode
יסוד העדיפות הוא לא להעלות הכול. ב-Windows רגיל, “מעלים רק את הת’רדים החשובים, ומורידים כראוי את עבודות הרקע” עובד טוב יותר. background mode הוא מנגנון שמתייחס בעדיפות נמוכה לא רק ל-CPU אלא גם למשאבים כמו I/O.
flowchart TD
accTitle: חלוקת העבודה בין ת'רדים ועדיפויות
accDescr: תרשים המראה חלוקה של העבודה לת'רד רגיש לדדליין, לת'רד עבודה ולממשק המשתמש, עם עדיפות גבוהה יותר או MMCSS לראשון, מצב רקע לשני ועדיפות רגילה לשלישי, ובלי לעבור מלכתחילה ל-REALTIME_PRIORITY_CLASS.
Work["מחלקים עבודה"] --> Critical["ת'רד רגיש לדדליין"]
Work --> Worker["שמירה / שליחה / דחיסה / סיכום"]
Work --> UI["UI"]
Critical --> P1["עדיפות גבוהה יותר או MMCSS, במידת הצורך"]
Worker --> P2["background mode / עדיפות נמוכה יותר"]
UI --> P3["עדיפות רגילה"]
P1 --> Warn["לא הופכים ל-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");
}
flowchart TB
accTitle: רישום וביטול ת'רד ל-MMCSS
accDescr: תרשים המראה שלפני הלולאה הרגישה לזמן נרשמים ל-MMCSS עם AvSetMmThreadCharacteristicsW, ואחרי שהלולאה מסתיימת חוזרים עם AvRevertMmThreadCharacteristics, וש-MMCSS מקצה CPU בעדיפות לעיבוד רגיש-זמן.
reg["AvSetMmThreadCharacteristicsW"] --> loop["מריצים לולאה רגישה לזמן"]
loop --> rev["AvRevertMmThreadCharacteristics"]
reg -.-> mm["MMCSS מקצה CPU בעדיפות"]
איור 13: משתמשים ב-MMCSS כזוג של רישום וביטול. בעיבוד מאגר רציף, זה תואם יותר את תכנון Windows מהרצה קבועה בעדיפות גבוהה.
4.4. זיכרון / GC / עלות ראשונית
אם משתמשים ב-hot path בכל פעם ב-new / malloc / List<T>.Add / חיבור מחרוזות / LINQ, מוקדם או מאוחר צרכי האיסוף או הסידור מחדש יעלו לפני השטח. GC (איסוף אשפה) בעצמו אינו רע, אך אם כותבים קוד עתיר הקצאות, ההשפעה שלו מופיעה כ-jitter.
flowchart LR
accTitle: חימום לפני המדידה
accDescr: תרשים המראה שאחרי ההפעלה מקצים את המאגרים הדרושים, נוגעים בהם פעם אחת כדי לחמם עמודים, מסיימים JIT, טעינת DLL ו-I/O ראשוני, ורק אחר כך מודדים או מריצים בפועל.
Start["הפעלה"] --> Alloc["מקצים את המאגרים הדרושים"]
Alloc --> Touch["נוגעים פעם אחת כדי לחמם עמודים"]
Touch --> Warm["מסיימים JIT, טעינת DLL, ו-I/O ראשוני"]
Warm --> Measure["רק אחר כך מודדים / מריצים בפועל"]
איור 14: מקצים מאגרים, מחממים עמודים, ומסיימים עלות ראשונית בזמן ההפעלה, ורק אז נכנסים למדידה / הפעלה אמיתית.
רשימת בדיקה
- אין הקצאה / שחרור זיכרון בכל פעם ב-hot path
- המאגרים הדרושים מוקצים מראש בזמן ההפעלה
- נוגעים פעם אחת בזמן ההפעלה כדי לחמם עמודים
- ה-JIT הראשוני, טעינת ה-DLL הראשונה, וה-I/O הראשוני לא מעורבים במדידה האמיתית
- לא מגדלים מבנה ענק או יומן באורך משתנה בתוך הלולאה
- גם אם משתמשים ב-
VirtualLock, זה מוגבל לאזור קריטי קטן מאוד
בדיקת צד .NET
- למדידת זמן משתמשים ב-
Stopwatch/Stopwatch.GetTimestamp() - אין LINQ, חיבור מחרוזות,
ToString(), או יצירת יומן ענק ב-hot path - לא מכניסים
async/awaitל-hot path - מעריכים בנפרד לפני ואחרי החימום
4.5. הגדרות צריכת חשמל / EcoQoS / רזולוציית טיימר
זה חלק לא בולט אבל יעיל. גם אם מסדרים את הקוד, אם בקרת צריכת החשמל ברמה הגבוהה יותר פעילה בעוצמה, התוצאה לא תהיה יציבה.
flowchart TD
accTitle: הגדרות צריכת החשמל
accDescr: תרשים המונה את מה שבודקים בסביבת צריכת החשמל של Windows רגיל: הפעלה על חשמל AC, מצב צריכת חשמל המכוון לביצועים, תוכנית חשמל ייעודית לסביבת ייצור, הימנעות מ-EcoQoS בתהליך רגיש-זמן, ובדיקת הטיפול בבקשת רזולוציית הטיימר.
Power["סביבת צריכת החשמל ב-Windows רגיל"] --> AC["מפעילים על חשמל AC"]
Power --> Mode["מצב צריכת חשמל: כיוון לביצועים מיטביים"]
Power --> Plan["במידת הצורך, תוכנית חשמל ייעודית לסביבת ייצור"]
Power --> QoS["נמנעים מ-EcoQoS בתהליך רגיש-זמן"]
Power --> Timer["בודקים את הטיפול בבקשת רזולוציית הטיימר"]
איור 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, שניהם חוזרים למערכת ההפעלה (התנהגות ברירת מחדל).
flowchart TB
accTitle: תפקיד שתי המסכות בהגדרת power throttling
accDescr: תרשים המראה שבוחרים את המנגנון שנשלט על ידי ControlMask, וקובעים אם הוא פועל או כבוי עם StateMask, וכך אפשר להצהיר שלא נופלים ל-EcoQoS ושבקשת רזולוציית הטיימר לא מתעלמים ממנה.
cm["ControlMask בוחר יעד שליטה"] --> sm["StateMask קובע פועל וכבוי"]
sm --> r1["הצהרה שלא נופלים ל-EcoQoS"]
sm --> r2["בקשת רזולוציית הטיימר לא מתעלמים ממנה"]
cm -.-> zero["אם 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).
flowchart LR
accTitle: סדר הצעדים בשיוך ליבות
accDescr: תרשים המראה שקודם מודדים, אחר כך מנסים SetThreadIdealProcessor או CPU Sets, ורק אם השיפור אינו מספיק שוקלים בסוף SetThreadAffinityMask, תוך בדיקה מקבילה של טמפרטורה, שעון והפעלה ארוכה.
Measure["קודם מודדים"] --> Ideal["SetThreadIdealProcessor / CPU Sets"]
Ideal --> Check{"השתפר מספיק?"}
Check -->|"כן"| Keep["עוצרים כאן"]
Check -->|"לא"| Hard["בסוף שוקלים SetThreadAffinityMask"]
Measure --> Therm["באותו זמן בודקים גם טמפרטורה / שעון / הפעלה ארוכה"]
איור 17: הקצאת CPU מתחילה במדידה, אם ציון רך מספיק עוצרים שם, ונעילה לליבה ספציפית היא המוצא האחרון.
רשימת בדיקה
- קודם מודדים, ורק אז נוגעים בהקצאת CPU
- לא ננעלים ישר לליבה ספציפית
- בודקים קודם את
SetThreadIdealProcessorאו CPU Sets SetThreadAffinityMaskנחשב מוצא אחרון- בהפעלה ארוכת טווח בודקים טמפרטורה, שעון וויסות עקב חום
- בודקים מצב שקט או רעש נמוך במחשב נייד
הסדר הבטוח הוא הזרימה הזו.
- קודם מודדים
- במידת הצורך, ideal processor / CPU Sets
- אם עדיין נדרש שיפור, נעילה לליבה ספציפית
נעילה לליבה ספציפית נראית יעילה, אבל היא מקטינה את דרכי המילוט של מערכת ההפעלה, ולכן שימוש לא זהיר בה עלול להפוך את המצב לפחות גמיש.
4.7. הפרדת דרייבר / DPC / ISR / הפרעות חיצוניות
כשקורה “לפעמים רק המקסימום מתפוצץ” או “הממוצע טוב אבל p99.9 גרוע”, כדאי לחשוד גם בהפרעות חיצוניות מחוץ לקוד האפליקציה.
flowchart TD
accTitle: עץ ההחלטה כשמופיעה קפיצה
accDescr: תרשים המראה סדר בידוד כשמופיעה קפיצת late, miss או max: תחילה זמן העיבוד של האפליקציה, אחר כך DPC ו-ISR, אחר כך page fault, GC ועלות ראשונית, אחר כך סוללה, חיסכון בחשמל וחום, ולבסוף חפירה עם ETW, WPA או LatencyMon.
Spike["הופיעה קפיצת late / miss / max"] --> Q1{"גם זמן העיבוד שלכם ארוך?"}
Q1 -->|"כן"| App["קיצור hot path / הפחתת הקצאות / הסרת I/O"]
Q1 -->|"לא"| Q2{"יש קפיצת DPC / ISR?"}
Q2 -->|"כן"| Driver["בודקים USB / Wi-Fi / Bluetooth / GPU / אודיו / אחסון / ACPI / עדכון דרייבר"]
Q2 -->|"לא"| Q3{"יש page fault / GC / עלות ראשונית?"}
Q3 -->|"כן"| Mem["הקצאה מראש / חימום / הפחתת עומס בערמה"]
Q3 -->|"לא"| Q4{"יש השפעה של סוללה / חיסכון בחשמל / חום?"}
Q4 -->|"כן"| Pow["חשמל AC / הגדרות צריכת חשמל / קירור / בדיקה ארוכת טווח"]
Q4 -->|"לא"| ETW["חפירה עם 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 למקסימום.
flowchart TB
accTitle: ההבדל בין הממוצע לבין המדדים בצד הזנב
accDescr: תרשים המראה שכשמסתכלים רק על הממוצע, ההשהיה הגדולה שמופיעה מדי פעם מוסתרת, בעוד שכשמסתכלים על p99, p99.9 ומקסימום, נראה הזנב האיטי, וב-Windows רגיל הבעיה האמיתית מופיעה שם.
avg["מסתכלים רק על הממוצע"] --> hide["ההשהיה הגדולה שמופיעה מדי פעם מוסתרת"]
tail["מסתכלים על p99, p99.9 ומקסימום"] --> see["נראה הזנב האיטי"]
see --> real["הבעיה האמיתית מופיעה בין p99 למקסימום"]
איור 19: גם אם הממוצע טוב, אם המקסימום קופץ, זה מצב “בדרך כלל מהיר אבל קופץ מדי פעם”. תופסים את זה עם מדדי הזנב.
נציין שהמאמר הזה אינו מציג ערכים מספריים להשפעת המבנה המומלץ. השהיה ו-jitter משתנים בקלות לפי CPU, דרייבר, תוכנה נלווית, הגדרות צריכת חשמל ואופן העמסת העומס, ולכן ערכים ממחשב של מישהו אחר לא משמשים כשלעצמם הוכחה עבור הסביבה שלכם. במקום זאת, נציג נוהל ליצירת אותה טבלה עבור הסביבה שלכם.
הנוהל המינימלי להוצאת p99 בסביבה שלכם
- ב-hot path מתעדים בלבד. לוקחים lateness וזמן ריצה עם
Stopwatch.GetTimestamp()(ב-C++:QueryPerformanceCounter), וכותבים למערך שהוקצה מראש. אסור לחשב כאן ממוצע או מיון - עוצרים את המדידה, ורק אז מרכזים. ממיינים, ומוציאים את הערך במיקום האחוזון
- חוזרים על אותו נוהל בשינוי תנאים: לפני/אחרי חימום, AC/סוללה, UI קדמי/ממוזער, עם/בלי עומס מתהליכים אחרים (5.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 מזהים חשוד בתנודה שמקורה בדרייבר
- ניטור טמפרטורה / שעון בודקים את השפעת החום
flowchart LR
accTitle: מה מודדים ואיך מחליטים
accDescr: תרשים המראה שמדידה פנימית באפליקציה נותנת התפלגות אחוזונים ומדדי miss, drop ועומק תור, ש-ETW, WPR ו-WPA מגלים context switch, DPC, ISR ו-page fault, ושניטור טמפרטורה ושעון מצטרף אליהם כדי לקבוע סדר עדיפות לשיפור.
App["מדידה פנימית באפליקציה"] --> Dist["p50 / p95 / p99 / p99.9 / max"]
App --> Miss["miss / drop / עומק תור"]
ETW["ETW / WPR / WPA"] --> Root["context switch / DPC / ISR / page fault"]
Temp["ניטור טמפרטורה / שעון"] --> Root
Dist --> Decide["קובעים סדר עדיפות לשיפור"]
Miss --> Decide
Root --> Decide
איור 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 רגיל נוטה להיות התנהגות שנגררת אחרי “אופן השימוש”, חשוב לבדוק תוך התקרבות לתנאים בפועל.
flowchart TB
accTitle: החשיבה שמאחורי בדיקה בתנאים נפרדים
accDescr: תרשים המראה שהערכה רק בסביבת benchmark שקטה מפספסת בעיות מהתפעול בפועל, ולכן בודקים בנפרד תנאים כמו לפני/אחרי חימום והפעלה ארוכה, UI קדמי מול ממוזער, וחשמל AC מול סוללה ועומס.
bench["הערכה רק בסביבת benchmark שקטה"] -.-> lose["מפספסים בעיות מהתפעול בפועל"]
lose -.-> split["בודקים בתנאים נפרדים"]
split --> c1["חימום והפעלה ארוכה"]
split --> c2["UI קדמי וממוזער"]
split --> c3["תנאי חיבור לחשמל ועומס"]
איור 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) עד לרמת התהליך, ולא רק הת’רד, היא גם אפשרות. כשמפרידים לתהליכים, גם אם השלב המאוחר נתקע, המחזור של השלב המוקדם ממשיך לרוץ.
flowchart TB
accTitle: השוואה בין מבנה משותף להפרדת תהליכים
accDescr: תרשים המראה שכשמחזיקים GUI, יומן, תקשורת ו-DB באותה לולאה של אותו תהליך, נטיית העניינים בשלב המאוחר שוברת את הדדליין של השלב המוקדם, ואילו הרחבת הפרדת fast path/slow path לרמת התהליך שומרת על המחזור המוקדם גם כשהשלב המאוחר נתקע.
one["מחזיקים הכול בתהליך אחד ולולאה אחת"] --> broke["נטיית השלב המאוחר שוברת את הדדליין המוקדם"]
broke -.-> ex["המתנה ל-flush, חיבור מחדש, ציור מחדש"]
sep["מפרידים בין השלב המוקדם למאוחר לפי תהליך"] --> keep["גם אם השלב המאוחר נתקע, המוקדם ממשיך לרוץ"]
איור 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. מקורות
- Multimedia Class Scheduler Service
- AvSetMmThreadCharacteristicsW function
- SetThreadPriority function
- SetPriorityClass function
- timeBeginPeriod function
- CreateWaitableTimerExW function
- Acquiring high-resolution time stamps
- QueryPerformanceCounter function
- GetSystemTimePreciseAsFileTime function
- SetProcessInformation function
- VirtualLock function
- CPU Sets
- SetThreadIdealProcessor function
- SetThreadAffinityMask function
- Processor power management options
- Change the power mode for your Windows PC
- Power settings in Windows 11
- CPU Analysis (WPA / WPT)
- WPR Command-Line Options
- Download and install the Windows ADK
- Quality of Service (EcoQoS / HighQoS)
- PROCESS_POWER_THROTTLING_STATE structure
- LatencyMon - Resplendence Software
מאמרים קשורים
מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.
למה עדיף המתנה מונחית אירועים על פני Sleep(1) ב-Windows
ב-Windows הדיוק של timed wait קצר מושפע מרזולוציית שעון המערכת ומהתזמון. המאמר מסביר מדוע כשמחכים להגעת עבודה, השלמת קלט/פלט או בקשת עציר...
טבלת החלטה: לסיים או להמשיך אחרי חריגה בלתי צפויה
המאמר מסדר מתי כדאי לסיים אפליקציה ומתי אפשר להמשיך לפעול אחרי חריגה בלתי צפויה, מנקודת המבט של השחתת מצב, תופעות לוואי חיצוניות, threads...
רשימת בדיקה מינימלית לאבטחה בפיתוח יישומי Windows
מסדרים בצורת רשימת בדיקה את היסודות של הרשאות, חתימה, עדכונים, סודות, HTTPS, אימות קלט, טעינת DLL ולוגים ביישומים עסקיים ב-WPF / WinForm...
למה להשתמש ב-Generic Host וב-BackgroundService של .NET באפליקציית desktop
בכלי Windows ובאפליקציות שרצות ברקע, מסודר כאן איך להשתמש ב-Generic Host וב-BackgroundService כדי לסדר הפעלה, עיבוד תקופתי, סיום, לוג, הג...
מדריך מעשי ל-FileSystemWatcher — התמודדות עם פספוסים וכפילויות
המאמר מסדר את אופן השימוש ואת נקודות התשומת לב של FileSystemWatcher, מנקודת המבט של פספוסים, התראות כפולות, מלכודות בקביעת סיום, סריקה חו...
נושאים קשורים
העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.
נושאים טכניים ב-Windows
שער לנושאי פיתוח Windows, חקירת תקלות וניצול נכסים קיימים.
שאלות נפוצות
שאלות נפוצות בפניות בנושא המאמר.
- האם אפשר לבצע עיבוד בזמן אמת ב-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, עומק תור, ותנודות טמפרטורה/שעון, ולהעריך בנפרד תנאים כמו לפני/אחרי חימום, הפעלה ארוכת טווח, מצב ממוזער, והפעלה על סוללה.