היסטוריית עדכונים (גרסה ראשונה, פורסמה בתאריך 9 Mar 2026)
- פרסום ראשון
לצטט את המאמר הזה(DOI: 10.5281/zenodo.22173307)
מאמר זה מאוחסן בארכיון Zenodo. להלן גם ה-DOI שתמיד מפנה לגרסה האחרונה וגם ה-DOI המקובע לגרסה שאתם קוראים.
Go Komura (2026). soft real-time ב-Windows רגיל: מדריך מעשי. KomuraSoft LLC. https://doi.org/10.5281/zenodo.22173307 https://comcomponent.com/he/blog/windows-soft-realtime-practical-guide-natural/
- DOI (הגרסה האחרונה)
- 10.5281/zenodo.22173307
- DOI (הגרסה הזו)
- 10.5281/zenodo.22173308
כשבונים ב-Windows עיבוד ש"אי אפשר שיאחר" — periodic processing, אודיו, וידאו, מדידה, בקרת ציוד — הרבה פעמים שומעים ש"ב-Windows זה לא יעבוד". התחושה הזו נכונה חלקית. Windows אינו hard real-time OS, אבל אם מתכננים, מממשים, מודדים ומפעילים כמו שצריך, אפשר להגיע למצב די מעשי כ-soft real-time.
המאמר עוסק ב-Windows 10 / 11 רגיל: בלי הרחבות RTOS, בלי kernel driver עצמאי, בלי בקר ייעודי. השאלה המעשית היא עד כמה אפשר לצמצם latency ו-jitter באפליקציית user-mode על desktop / laptop רגיל. באודיו, וידאו, בקרה מחזורית ואיסוף נתונים הפרטים שונים, אבל המקומות שנוטים להישבר משותפים למדי. לכן ריכזתי כאן את החלק המשותף כ-checklist.
למי זה מיועד, ובאיזו שפה דוגמאות הקוד
המאמר כתוב ל-מפתחים שבונים ב-Windows עיבוד ש"אי אפשר שיאחר" (בקרה מחזורית, אודיו/וידאו, מדידה, בקרת ציוד). מדובר בפיתוח אפליקציות user-mode. מימוש kernel-mode drivers מחוץ להיקף.
שפת דוגמאות הקוד מחולקת כך:
| תוכן | שפה | איפה |
|---|---|---|
| periodic loop, MMCSS, power QoS וכדומה — חלקים שקוראים ישירות ל-Win32 API | C++ (Win32) | 4.1, 4.3, 4.5 |
| אותה קריאה ל-Win32 API מ-C# | C# (P/Invoke) | 4.5 |
| מדידת זמן, GC, allocation | .NET (C#) | הבדיקות בצד .NET בסעיף 4.4, וסעיף 5.2 |
הדיון בגורמים עד פרק 3, וגוף ה-checklist בפרק 4, לא תלויים בשפה. מי שעובד רק ב-C# יכול לקרוא את קוד ה-C++ פשוט כהסבר איזה API קוראים ובאיזה סדר.
תוכן עניינים
- המסקנה בקצרה
- 1.1. טבלת קריאה מהירה לפי טווח מחזור
- מה זה soft real-time ב-Windows רגיל
- 2.1. מה נקרא כאן "Windows רגיל"
- 2.2. מה ריאלי, ומאיפה זה נהיה קשה
- 2.3. מונחים, במשפט
- הגורמים העיקריים ל-latency ול-jitter
- 3.1. scheduler ו-priority
- 3.2. DPC / ISR ו-drivers
- 3.3. page fault וזיכרון
- 3.4. timer resolution ו-power management
- 3.5. מעבר בין cores וחום
- checklist מעשי להקטנת latency ב-Windows רגיל
- 4.1. periodic loop ואיך ממתינים
- 4.2. fast path / slow path ו-queue באורך קבוע
- 4.3. priority / MMCSS / background mode
- 4.4. זיכרון / GC / עלות ראשונה
- 4.5. הגדרות power / EcoQoS / timer resolution
- 4.6. שיבוץ CPU / מעבר בין cores / חום
- 4.7. isolation של driver / DPC / ISR / הפרעות חיצוניות
- מדידה והערכה
- 5.1. מה מתעדים
- 5.2. איך קוראים p99 / p99.9 / max
- 5.3. באילו כלים מסתכלים
- 5.4. איך בודקים
- איך בוחרים בפועל
- סיכום
- מקורות
ב-diagram, solid line מציינת relation שתמיד מתקיים ו-dashed line מציינת relation מותנה (התנאים מופיעים בהסבר של כל relation ב-detail page). הרשימה המלאה של ה-relations (סה”כ 20, כולל evidence ו-certainty) וההגדרות של ה-concepts המרכזיים נמצאות ב-detail page של ה-knowledge map (ביפנית). Data: JSON-LD / Turtle
1. המסקנה בקצרה
- ב-Windows רגיל היעד אינו hard real-time. היעד הוא soft real-time: פחות איחורים, וגם כשמאחרים — המערכת לא נשברת.
- מה שהכי משפיע: לקצר את ה-hot path, לשמור אותו fixed-size, ולשמור אותו non-blocking.
- מפרידים fast path (acquisition / בקרה) מ-slow path (שמירה / תקשורת / UI), ומחברים ביניהם ב-queue באורך קבוע.
- את ה-periodic loop מריצים לפי absolute deadline, לא לפי
Sleep(1). - בזרם רציף כמו אודיו או וידאו, בודקים קודם MMCSS.
- למדידת זמן משתמשים ב-QueryPerformanceCounter (QPC); ב-.NET ב-
Stopwatch. - להמתנה מעדיפים device event או waitable timer ברזולוציה גבוהה.
- את
timeBeginPeriodקוראים רק כל עוד צריך. לא מתכננים בהנחה שהוא דולק תמיד. - בתפעול בפועל עוזרים AC power / power mode / הטיפול ב-EcoQoS / צמצום עומס ברקע.
- מעריכים לא רק לפי ממוצע, אלא לפי p99 (הסף שבו מתחיל להיראות 1 מתוך 100 המדידות האיטיות) / p99.9 / max / מספר misses / DPC / ISR / page fault / עומק queue.
במילים אחרות, ב-Windows רגיל עדיף לצמצם בתכנון את הסיבות לאיחור, מאשר להעלות priority. priority והגדרות power חשובות, אבל לבדן הן לא מייצרות יציבות.
flowchart TB
accTitle: יציבות מגיעה מהתכנון, לא מ-priority
accDescr: קיצור ה-hot path והפיכתו ל-fixed-size ול-non-blocking מקטינים בתכנון את הסיבות לאיחור, ומגיעים למבנה שמאחר פחות ולא נשבר. priority והגדרות power חשובות, אבל לבדן לא מספיקות.
hot["hot path קצר ו-fixed-size"] --> less["פחות סיבות לאיחור כבר בתכנון"]
less --> goal["מבנה שמאחר פחות ולא נשבר"]
prio["כיוונון priority והגדרות power"] -.-> goal
prio -.-> note["חשוב, אבל לא מספיק לבד"]
איור 1: מה שהכי משפיע אינו כיוונון priority, אלא לצמצם בתכנון את הסיבות לאיחור עצמן.
1.1. טבלת קריאה מהירה לפי טווח מחזור
המאמר ארוך, לכן הטבלה הזו מאפשרת לקרוא רק את השורה שמתאימה למקרה שלכם. התוכן שישב פעם בסוף (פרק 6) הועבר לכאן.
| מחזור / דרישה | המבנה הראשוני | הסעיפים שכדאי לקרוא |
|---|---|---|
| סדר גודל 10-20ms, אפשר לספוג תנודות מדי פעם | הפרדת fast path / slow path, queue באורך קבוע, priority רגילה עד מעט גבוהה, event-driven. לרוב זה מספיק | 4.1, 4.2 |
| סדר גודל 1-5ms, רוצים לעמוד בזה באופן רציף | בנוסף: בלי allocations ב-hot path, thread ייעודי, MMCSS או כיוונון priority זהיר, waitable timer ברזולוציה גבוהה, AC power ובדיקת הגדרות power | 4.1 עד 4.5 |
| מתקרבים ל-מתחת ל-1ms, ולא רוצים לפספס גם בריצה ארוכה ובעומס גבוה | קשה מאוד עם user-mode לבד ב-Windows רגיל. כדאי לשקול קודם תכנון שמעביר את החלק הקריטי החוצה (firmware בצד ה-device, בקר ייעודי, FPGA, RTOS) | 2.2, 6 |
| רוצים הכול באותו מקום — GUI / logging / תקשורת / DB | לא לגורר הכול ב-“process אחד, loop אחד”. מחלקים אחריות. העבודה בשלב המאוחר נוטה לשבור את ה-deadline של השלב המוקדם | 4.2, 4.3, 6 |
isolation של גורמים ושיטת המדידה משותפים לכל טווח (פרקים 3 ו-5).
2. מה זה soft real-time ב-Windows רגיל
2.1. מה נקרא כאן “Windows רגיל”
Windows רגיל, כאן, מניח בערך את הדברים הבאים.
- desktop / laptop רגיל עם Windows 10 / 11
- בלי הרחבת RTOS עצמאית
- בלי פיתוח kernel-mode driver עצמאי
- אפליקציית user-mode רגילה
- כיוונון עם Windows API והגדרות רגילות
כלומר, זה לא סיפור של “לבנות מכונה ייעודית אחת כולה לבקרת real-time”, אלא “עד כמה אפשר לצמצם באופן מעשי על PC Windows רגיל”.
flowchart LR
accTitle: היקף המאמר
accDescr: המאמר עוסק באפליקציית user-mode על PC Windows רגיל שמכוונת ל-soft real-time — latency נמוך, פחות jitter, ומעקב אחרי deadline misses — ודרישה לאפס misses שייכת ל-RTOS, בקר ייעודי, FPGA או עיבוד בצד ה-device.
A["PC Windows 10 / 11 רגיל"] --> B["אפליקציית user-mode"]
B --> C["מכוונים ל-soft real-time"]
C --> D["מורידים latency"]
C --> E["מקטינים jitter"]
C --> F["מנטרים deadline misses ולא נשברים"]
G["רוצים להבטיח אפס deadline misses"] -.-> H["RTOS / בקר ייעודי / FPGA / עיבוד בצד ה-device"]
איור 2: באפליקציית user-mode ב-Windows רגיל מכוונים ל-soft real-time. אפס deadline misses שייך ל-RTOS וכדומה.
2.2. מה ריאלי, ומאיפה זה נהיה קשה
גם ב-Windows רגיל אפשר להגיע למצב מעשי של “מאחר פחות” בעיבודים כמו אלה.
- periodic processing של כמה ms עד כמה עשרות ms
- audio / video שמבוססים על buffer
- קריאה מחיישנים ו-control loops
- עיבוד מחזורי קבוע בסגנון soft PLC
- pipeline בהשהיה נמוכה שרץ ב-thread נפרד מה-UI
עם זאת, “אפשר” כאן אינו אומר שאפשר לאפס לגמרי latency spikes מדי פעם. המטרה היא המצב הבא.
- להוריד את ה-latency הרגיל
- להקטין jitter
- לא להישבר גם כשמפספסים deadline מדי פעם
- לראות בבירור שפספסנו
לעומת זאת, כשהדרישה היא כזו, קשה מאוד לספק אותה עם user-mode ב-Windows רגיל בלבד.
- רוצים להבטיח אפס deadline misses
- רוצים לשמור בעקביות על פחות ממאות מיקרו-שניות לאורך זמן
- רוצים לחיות יחד עם GUI כבד, רשת ו-storage
- רוצים לעשות זאת על סוללה או במצב חיסכון בחשמל
- אסור שיופיעו גם spikes שמקורם ב-driver או ב-device
במקרים כאלה, בטוח יותר לשקול להעביר רק את החלק שבאמת דורש דיוק זמנים חמור ל-firmware בצד ה-device, לבקר ייעודי, ל-FPGA או ל-RTOS.
flowchart TB
accTitle: המצב שמכוונים אליו ב-soft real-time, ומתי מעבירים החוצה
accDescr: ב-Windows רגיל מכוונים להוריד latency רגיל, להקטין jitter, ולא להישבר תוך יכולת לנטר כשמפספסים deadline מדי פעם. דרישה כמו אפס deadline misses שווה להעביר החוצה רק את החלק שדורש דיוק זמנים.
aim["המצב שמכוונים אליו ב-soft real-time"] --> s1["הורדת ה-latency הרגיל"]
aim --> s2["הקטנת jitter"]
aim --> s3["לא נשברים גם כשמפספסים, וניתן לנטר"]
strict["דרישה של אפס deadline misses"] -.-> out["מעבירים ל-device או RTOS"]
איור 3: “אפשר” אינו אפס spikes. זה מצב שמאחר פחות, נשבר פחות, וניתן לנטר.
2.3. מונחים, במשפט
לפני שנכנסים לגוף, הנה המשמעות של המונחים במאמר.
| מונח | במשפט | הזווית המעשית |
|---|---|---|
| soft real-time | מקבלים שאיחור מדי פעם אפשרי, מקטינים אותו, ולא נשברים גם כשהוא קורה | זה מה שמכוונים אליו קודם ב-Windows רגיל |
| hard real-time | עולם שבו רוצים להבטיח אפס deadline misses | לא יעד מעשי ל-user-mode לבד ב-Windows רגיל |
| jitter | תנודה במחזור או בזמן התגובה | גם אם הממוצע טוב, jitter גדול לא יציב בתפעול |
| deadline miss | העיבוד לא מסתיים עד הזמן המתוכנן | לא מסתירים, סופרים, וכותבים ללוג |
| p99 / p99.9 | מדדים שמראים את הזנב האיטי | p99 הוא “הסף שבו מתחיל להיראות 1 מתוך 100 המדידות האיטיות” |
| DPC / ISR | עיבוד בצד ה-kernel סביב drivers ו-interrupts | אם הוא ארוך, thread ה-user-mode נאלץ לחכות |
| MMCSS | מנגנון ב-Windows שמקצה CPU לעיבוד רגיש-זמן כמו אודיו / וידאו | שימושי בעיבוד שלא רוצים שה-buffer שלו יתרוקן |
| QPC | כינוי ל-QueryPerformanceCounter |
הבסיס למדידת זמן שעבר. לא שעון קיר, אלא מונה ברזולוציה גבוהה |
| waitable timer | אובייקט kernel שהופך ל-signaled בזמן שנקבע. אם מוסיפים CREATE_WAITABLE_TIMER_HIGH_RESOLUTION ל-CreateWaitableTimerExW, מקבלים גרסה ברזולוציה גבוהה |
מתאים כבסיס להמתנה מחזורית יותר מ-Sleep (4.1) |
| EcoQoS | מצב שסווג כ”מותר לתת עדיפות לחיסכון בחשמל”. יכול להוריד תדר CPU או להעביר ל-cores חסכוניים | נמנעים ממנו בעיבוד רגיש-זמן. אם לא מציינים במפורש, ה-OS מנחש אוטומטית (4.5) |
IGNORE_TIMER_RESOLUTION |
ציון “מותר להתעלם מבקשת timer resolution של ה-process” (PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION) |
אם זה דולק, האפקט של timeBeginPeriod נעלם. ב-Windows 11 זה יכול לחול אוטומטית כשהחלון מוסתר (4.5) |
| CPU Sets | מנגנון לציון רך מסוג “רוצים שהריצה תהיה בקבוצת ה-cores הזו” עבור thread או process | בודקים לפני pin ל-core ספציפי (4.6) |
| ETW / WPR / WPA | תשתית ה-trace הסטנדרטית של Windows (ETW), כלי ההקלטה שלה (WPR) וה-GUI לניתוח (WPA) | משמש לחפירה ב-context switch, DPC / ISR ו-page fault (5.3) |
| LatencyMon | כלי צד-שלישי לבדיקת latency שמקורו ב-driver | סוקר את זמני ה-DPC / ISR לפי driver כדי לזהות חשוד (5.3) |
3. הגורמים העיקריים ל-latency ול-jitter
הסיבה שבגללה periodic processing מאחר ב-Windows רגיל מגיעה בדרך כלל לאחד מהענפים בתרשים הזה.
flowchart TD
accTitle: גורמים לאיחור ב-periodic processing
accDescr: חמישה כיווני חקירה כש-periodic processing מאחר: scheduler ו-priority, DPC ו-ISR ו-drivers, page fault וזיכרון, timer resolution ו-power management, ומעבר בין cores וחום.
Late["periodic processing מאחר"] --> S["scheduler / priority"]
Late --> D["DPC / ISR / driver"]
Late --> M["page fault / זיכרון"]
Late --> T["timer resolution / power management"]
Late --> C["מעבר בין cores / חום"]
איור 4: הסיבות לאיחור ב-periodic processing מגיעות לחמש מערכות — scheduler, DPC/ISR, זיכרון, timer ו-power, ומעבר בין cores וחום.
3.1. scheduler ו-priority
סדר הריצה של threads ב-Windows נקבע לפי priority. באותו priority רצים ב-round-robin, וכש-thread בעדיפות גבוהה יותר הופך ל-runnable, thread בעדיפות נמוכה יותר נדחק הצידה.
כלומר, גם אם כותבים periodic thread ברצינות, יכולים באופן שגרתי לרוץ לפניו:
- thread אחר
- process אחר
- עיבוד פנימי של ה-OS
- מוצר אבטחה
- עיבוד עזר של device
- סנכרון ברקע
flowchart TB
accTitle: דחיקה על ידי scheduling לפי priority
accDescr: threads באותו priority רצים ב-round-robin, וכש-thread בעדיפות גבוהה יותר הופך ל-runnable, thread בעדיפות נמוכה נדחק הצידה. לכן זה שגרתי ש-process אחר או עיבוד פנימי של ה-OS ירוצו לפני periodic thread.
same["threads באותו priority"] --> rr["רצים ב-round-robin"]
high["priority גבוה יותר הופך ל-runnable"] --> push["priority נמוך נדחק הצידה"]
push -.-> who["למשל process אחר או עיבוד פנימי של ה-OS"]
איור 5: גם כשכותבים periodic thread ברצינות, זה שגרתי שעבודה בעדיפות גבוהה יותר תרוץ לפניו.
3.2. DPC / ISR ו-drivers
זו נקודה חשובה מאוד. גם אם מסדרים את ה-priority בצד האפליקציה, אם DPC (Deferred Procedure Call) או ISR (Interrupt Service Routine) ארוכים, thread ה-user-mode לא יכול לרוץ באותו זמן.
ה-devices וה-drivers שנוטים לגרום לזה הם בערך אלה.
- USB
- Wi-Fi / Bluetooth
- storage
- audio
- GPU
- ACPI וסביבת power
גם כשהקוד של האפליקציה בסדר, אפשר להיעצר בגלל ה-driver או החומרה. כאן, אם חושבים “אם מעלים עוד את ה-priority של האפליקציה, ננצח” — בדרך כלל נכשלים בכאב.
flowchart TB
accTitle: איך DPC ו-ISR עוצרים את ה-user-mode
accDescr: כש-devices כמו USB, Wi-Fi ו-GPU מפעילים ISR או DPC ארוכים בצד ה-kernel, thread ה-user-mode לא יכול לרוץ באותו זמן, וגם העלאת priority של האפליקציה לא מנצחת את זה.
dev["devices כמו USB, Wi-Fi, GPU"] --> kern["ISR או DPC רצים בצד ה-kernel"]
kern --> block["באותו זמן ה-user-mode לא יכול לרוץ"]
block -.-> note["גם העלאת priority לא מנצחת"]
איור 6: גם כשהקוד של האפליקציה בסדר, ה-user-mode נעצר בגלל ה-driver או החומרה.
3.3. page fault וזיכרון
כשמתרחש page fault ב-hot path (העמוד הדרוש לא נמצא בזיכרון, ויוצאים להביא אותו), ה-latency קופץ בבת אחת.
תבניות שכדאי במיוחד להימנע מהן:
- commit של עמוד בגישה הראשונה
- lazy load
- page-in של memory-mapped file
- הקצאה דינמית מעבר לנדרש
- אובייקטים גדולים או heap מפוצל
בגוף ה-periodic processing, נכון להקצות מראש את הזיכרון הדרוש ולגעת בו פעם אחת בזמן ה-startup.
flowchart TB
accTitle: latency שנגרם מ-page fault ב-hot path
accDescr: אם העמוד שנוגעים בו ב-hot path לא נמצא בזיכרון, יוצאים להביא אותו ב-page fault וה-latency קופץ בבת אחת. לכן עדיף להקצות מראש ולגעת פעם אחת ב-startup.
touch["נוגעים בזיכרון ב-hot path"] --> q{"העמוד נמצא בזיכרון?"}
q -->|"כן"| ok["ממשיכים כרגיל"]
q -->|"לא"| pf["יוצאים להביא אותו ב-page fault"]
pf --> spike["ה-latency קופץ בבת אחת"]
pf -.-> fix["הקצאה מראש ונגיעה פעם אחת ב-startup"]
איור 7: ה-latency קופץ לפי אם מתרחש page fault. הפתרון הוא הקצאה מראש ו-warmup ב-startup.
3.4. timer resolution ו-power management
“רוצה להריץ כל 1ms, אז Sleep(1)” — כמעט תמיד לא עובד.
דיוק ההמתנה ב-Windows מושפע מ-timer resolution, מה-scheduler, וממצב ה-power.
מעבר לזה, הגדרה שמעלה את ה-timer resolution משפרת מעט את דיוק ההמתנה, אבל יש לה תופעות לוואי על צריכת חשמל ועל התנהגות המערכת כולה — נקודה שאי אפשר להתעלם ממנה.
3.5. מעבר בין cores וחום
כש-thread עובר בין cores, נוצר חימום מחדש של ה-cache. זה עצמו מטופל לרוב היטב על ידי ה-OS, אבל בסביבה עמוסה זה הופך לגורם תנודה.
מעבר לזה, בריצה ארוכה גם החום אי אפשר להתעלם ממנו. כשנכנס thermal throttling, המחזור שהיה יציב עד אז יכול להישבר.
flowchart TB
accTitle: המסלול שבו מעבר בין cores וחום שוברים את המחזור
accDescr: מעבר thread בין cores גורם לחימום מחדש של ה-cache שהופך לגורם תנודה בסביבה עמוסה, ובריצה ארוכה הצטברות חום מפעילה thermal throttling ששובר את המחזור שהיה יציב.
move["thread עובר בין cores"] --> cache["חימום מחדש של ה-cache"]
cache --> jitter["בסביבה עמוסה — גורם תנודה"]
heat["בריצה ארוכה מצטבר חום"] --> thr["thermal throttling"]
thr --> broke["המחזור היציב נשבר"]
איור 8: מעבר בין cores פוגע ביציבות דרך jitter, וחום פוגע בה דרך throttling — שניהם בריצה ארוכה.
4. checklist מעשי להקטנת latency ב-Windows רגיל
מכאן החלק המעשי. לגורמים שראינו למעלה, ה-checklist מרכז מה בודקים, ממה נמנעים, ומה מחליטים מראש ב-Windows רגיל.
4.1. periodic loop ואיך ממתינים
קודם כול, anti-pattern טיפוסי הוא זה.
while (running)
{
Sleep(1);
Step();
}
זו לא לולאה של “מחזור 1ms”. זו לולאה שבה ממתינים בערך 1ms ומעלה, ומעל זה מוסיפים את זמן הריצה של Step().
בנוסף, overshoot של ההמתנה מצטבר כמו שהוא.
flowchart LR
accTitle: המתנה יחסית מול absolute deadline
accDescr: השוואה בין לולאה מבוססת Sleep יחסי, שבה שגיאת ההמתנה וזמן הריצה מצטברים בהדרגה, לבין לולאה מבוססת absolute deadline שמקדמת את הזמן הבא בקבוע ולכן קשה לצבור בה drift.
subgraph Bad["מבוסס זמן יחסי"]
B1["Sleep(1)"] --> B2["Step()"]
B2 --> B1
end
B2 --> B3["שגיאת ההמתנה וזמן הריצה מצטברים בהדרגה"]
subgraph Good["מבוסס absolute deadline"]
G1["next += period"] --> G2["WaitUntil(next - margin)"]
G2 --> G3["spin קצר במידת הצורך"]
G3 --> G4["FastStep()"]
G4 --> G1
end
G4 --> G5["קשה לצבור drift"]
איור 9: לולאה מבוססת זמן יחסי שמסתמכת על Sleep(1) צוברת שגיאה. לולאה מבוססת absolute deadline קשה לה לצבור drift.
checklist
- לא מבססים את ה-periodic loop על
Sleep(1) - מריצים את המחזור לפי absolute deadline של
next += period - להמתנה מעדיפים device event או waitable timer
- רק את ה-fine-tune האחרון מגבילים ל-busy-spin קצר מאוד
- את
timeBeginPeriodקוראים רק כל עוד צריך, ומחזירים כשמסיימים - בודקים את ההתנהגות גם במצב ממוזער / מוסתר / לא נראה
ה-periodic loop יציב יותר כשהוא רץ לפי absolute deadline, לא לפי זמן יחסי.
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 ו-queue באורך קבוע
יסוד המבנה: ב-fast path שמים רק עבודה רגישה ל-deadline, ואת השאר מוציאים ל-slow path.
flowchart LR
accTitle: הפרדה בין fast path ל-slow path
accDescr: event מה-device מטופל ב-fast path עם עבודה מינימלית, עובר דרך queue באורך קבוע אל slow path שמבצע שמירה, שליחה, UI וסיכום, וה-fast path מתעד גם lateness, miss ועומק queue.
Input["device / event של acquisition"] --> Fast["fast path: acquisition, בקרה, העתקה מינימלית"]
Fast --> Queue["queue באורך קבוע"]
Queue --> Slow["slow path: שמירה, שליחה, UI, סיכום"]
Fast --> Metrics["תיעוד lateness / miss / עומק queue"]
Metrics --> Slow
איור 10: ב-fast path שמים רק עבודה רגישה ל-deadline, ומוציאים ל-slow path דרך queue באורך קבוע.
מה שעושים ב-fast path מצמצמים בערך לזה.
- acquisition של נתונים
- חישוב ערך בקרה
- העתקה מינימלית נדרשת
- timestamp
- הזנה ל-queue
- תיעוד miss / overrun
כל השאר יורד ל-slow path.
checklist
- אין כתיבה לקובץ, שליחה ברשת, כתיבה ל-DB ב-hot path
- אין logging כבד,
Flush, או RPC סינכרוני ב-hot path - מפרידים בבירור בין fast path ל-slow path לפי thread או אחריות
- ה-queue באורך קבוע
- מדיניות למקרה שה-queue מלא הוחלטה מראש
- מנטרים מספר miss, מספר drop, ועומק queue
- עדכון UI וריכוז לוגים מופרדים לתדירות נמוכה
כשה-queue מתמלא, בטוח יותר לא להשאיר את המדיניות מעורפלת.
flowchart TD
accTitle: מדיניות כשה-queue מתמלא
accDescr: שלוש מדיניות אפשריות כשה-queue מלא, לפי מה שחשוב לשמור: מחיקת ישן ושמירת העדכני, התראה ועצירה או בקרת המקור, או הפלת ישן עם תיעוד מספר ה-drop בלבד.
Overflow["ה-queue מלא"] --> Policy{"מה שומרים?"}
Policy -->|"הערך העדכני חשוב"| Latest["מוחקים ישן ומשאירים עדכני"]
Policy -->|"כל הרשומות חשובות"| All["התראה / עצירה / בקרת המקור"]
Policy -->|"לצורכי לוג"| Log["מפילים ישן, ומתעדים רק את מספר ה-drop"]
איור 11: מה שומרים כשה-queue מתמלא מחליטים מראש לפי מטרת השימוש.
4.3. priority / MMCSS / background mode
יסוד ה-priority: לא להעלות הכול. ב-Windows רגיל, “מעלים רק את ה-threads החשובים, ומורידים כראוי את עבודות הרקע” עובד טוב יותר. background mode הוא מנגנון שמתייחס בעדיפות נמוכה לא רק ל-CPU אלא גם למשאבים כמו I/O.
flowchart TD
accTitle: חלוקת העבודה בין threads ו-priorities
accDescr: חלוקה של העבודה ל-thread רגיש ל-deadline, ל-worker thread ול-UI, עם priority גבוה יותר או MMCSS לראשון, background mode לשני ו-priority רגילה לשלישי, ובלי לעבור מלכתחילה ל-REALTIME_PRIORITY_CLASS.
Work["מחלקים עבודה"] --> Critical["thread רגיש ל-deadline"]
Work --> Worker["שמירה / שליחה / דחיסה / סיכום"]
Work --> UI["UI"]
Critical --> P1["priority גבוה יותר או MMCSS, במידת הצורך"]
Worker --> P2["background mode / priority נמוכה יותר"]
UI --> P3["priority רגילה"]
P1 --> Warn["לא הופכים ל-REALTIME_PRIORITY_CLASS מלכתחילה"]
איור 12: איך מחלקים priority — מעלים רק את ה-thread הרגיש ל-deadline, ומורידים כראוי את עבודות הרקע.
checklist
- לא הופכים את כל ה-threads ל-priority גבוהה
- מעלים רק את ה-thread שבאמת רגיש לזמן
- עבודות רקע כמו שמירה, שליחה, דחיסה וסנכרון יורדות ל-background mode
- בעיבוד buffer רציף כמו אודיו, וידאו, capture או playback, שוקלים MMCSS
- חושבים קודם ברמת ה-thread, לא ברמת ה-process כולו
- לא משתמשים ב-
REALTIME_PRIORITY_CLASSעד שהצורך ברור
MMCSS (Multimedia Class Scheduler Service) יעיל במיוחד עבור עיבוד כמו אודיו / וידאו שרוצים למלא buffer בתוך זמן קצוב. זה תואם יותר לתכנון של Windows מאשר פשוט להריץ thread ב-priority גבוהה כל הזמן.
הרגשת הקוד נראית בערך כך.
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: רישום וביטול thread ל-MMCSS
accDescr: לפני הלולאה הרגישה לזמן נרשמים ל-MMCSS עם AvSetMmThreadCharacteristicsW, ואחרי שהלולאה מסתיימת חוזרים עם AvRevertMmThreadCharacteristics. MMCSS מקצה CPU בעדיפות לעיבוד רגיש-זמן.
reg["AvSetMmThreadCharacteristicsW"] --> loop["מריצים לולאה רגישה לזמן"]
loop --> rev["AvRevertMmThreadCharacteristics"]
reg -.-> mm["MMCSS מקצה CPU בעדיפות"]
איור 13: משתמשים ב-MMCSS כזוג של רישום וביטול. בעיבוד buffer רציף, זה תואם יותר את תכנון Windows מהרצה קבועה ב-priority גבוהה.
4.4. זיכרון / GC / עלות ראשונה
אם משתמשים ב-hot path בכל פעם ב-new / malloc / List<T>.Add / חיבור מחרוזות / LINQ, מוקדם או מאוחר צרכי ה-collection או ה-compaction יעלו לפני השטח. GC עצמו אינו רע, אך אם כותבים קוד עתיר allocations, ההשפעה מופיעה כ-jitter.
flowchart LR
accTitle: warmup לפני המדידה
accDescr: אחרי ה-startup מקצים את ה-buffers הדרושים, נוגעים בהם פעם אחת כדי לחמם עמודים, מסיימים JIT, טעינת DLL ו-I/O ראשוני, ורק אחר כך מודדים או מריצים בפועל.
Start["startup"] --> Alloc["מקצים את ה-buffers הדרושים"]
Alloc --> Touch["נוגעים פעם אחת כדי לחמם עמודים"]
Touch --> Warm["מסיימים JIT, טעינת DLL, ו-I/O ראשוני"]
Warm --> Measure["רק אחר כך מודדים / מריצים בפועל"]
איור 14: מקצים buffers, מחממים עמודים, ומסיימים עלות ראשונה ב-startup, ורק אז נכנסים למדידה / ריצה אמיתית.
checklist
- אין allocation / שחרור זיכרון בכל פעם ב-hot path
- ה-buffers הדרושים מוקצים מראש ב-startup
- נוגעים פעם אחת ב-startup כדי לחמם עמודים
- ה-JIT הראשוני, טעינת ה-DLL הראשונה, וה-I/O הראשוני לא מעורבים במדידה האמיתית
- לא מגדלים מבנה ענק או לוג באורך משתנה בתוך הלולאה
- גם אם משתמשים ב-
VirtualLock, זה מוגבל לאזור קריטי קטן מאוד
בדיקות בצד .NET
- למדידת זמן משתמשים ב-
Stopwatch/Stopwatch.GetTimestamp() - אין LINQ, חיבור מחרוזות,
ToString(), או יצירת לוג ענק ב-hot path - לא מכניסים
async/awaitל-hot path - מעריכים בנפרד לפני ואחרי warmup
4.5. הגדרות power / EcoQoS / timer resolution
זה חלק לא בולט אבל יעיל. גם אם מסדרים את הקוד, אם בקרת ה-power ברמה הגבוהה יותר פעילה בעוצמה, התוצאה לא תהיה יציבה.
flowchart TD
accTitle: הגדרות power
accDescr: מה בודקים בסביבת ה-power של Windows רגיל: הרצה על AC, power mode שמכוון לביצועים, תוכנית power ייעודית לסביבת ייצור, הימנעות מ-EcoQoS ב-process רגיש-זמן, ובדיקת הטיפול בבקשת timer resolution.
Power["סביבת ה-power ב-Windows רגיל"] --> AC["מריצים על AC"]
Power --> Mode["power mode: כיוון לביצועים מיטביים"]
Power --> Plan["במידת הצורך, תוכנית power ייעודית לסביבת ייצור"]
Power --> QoS["נמנעים מ-EcoQoS ב-process רגיש-זמן"]
Power --> Timer["בודקים את הטיפול בבקשת timer resolution"]
איור 15: הנקודות שבודקים בסביבת ה-power — חיבור לחשמל, power mode, EcoQoS, וטיפול בבקשת timer resolution.
checklist
- הערכת הייצור נעשית קודם על AC
[הגדרות] > [מערכת] > [חשמל וסוללה] > [מצב צריכת חשמל]מכוונים לכיוון ביצועים מיטביים- לא משתמשים במצב חיסכון בסוללה / חיסכון בחשמל בזמן הריצה
- בודקים מצבי שקט / eco / battery של כלים עצמאיים של היצרן
- לא משאירים process רגיש-זמן ב-EcoQoS (QoS מוטה חיסכון) בלי כוונה
IGNORE_TIMER_RESOLUTIONלא דולק בצד process רגיש-זמן- בודקים שהאפקט של בקשת timer resolution לא משתנה כשממזערים / מסתירים
- מפרידים הגדרות power לשימוש יומיומי מהגדרות לייצור / מדידה / הדגמה
timeBeginPeriod שימושי כשמשתמשים בו בזהירות, אבל הוא לא תרופת פלא.
- קוראים לו ממש לפני הצורך
- כשמסיימים, מחזירים עם
timeEndPeriod - מ-Windows 10 version 2004 ואילך, ההתנהגות אינה גלובלית מלאה כפי שהייתה בעבר
- ב-Windows 11, כש-process עם חלון מוסתר לגמרי / ממוזער / לא נראה / לא נשמע, ייתכן שהרזולוציה הגבוהה לא מובטחת
- העלאת הרזולוציה לא משפרת את דיוק ה-QPC
אם ההשפעה של power או 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 (מוטה ביצועים) + כיבוד בקשת timer resolution
if (!SetProcessInformation(
GetCurrentProcess(),
ProcessPowerThrottling,
&state,
sizeof(state)))
{
throw std::runtime_error("SetProcessInformation failed");
}
ControlMask הוא “אילו מנגנונים שולטים בעצמכם”, ו-StateMask הוא “האם מפעילים או מכבים את המנגנון הזה”. הדוגמה למעלה בוחרת שני מנגנונים לשליטה, ומכבה את שניהם. כלומר, זו הצהרה שלא נופלים ל-EcoQoS (מוטה HighQoS), ושבקשת timer resolution לא מתעלמים ממנה. לעומת זאת, אם הופכים את ControlMask ל-0, שניהם חוזרים ל-OS (התנהגות ברירת מחדל).
flowchart TB
accTitle: תפקיד שתי המסכות בהגדרת power throttling
accDescr: בוחרים את המנגנון שנשלט על ידי ControlMask, וקובעים אם הוא פועל או כבוי עם StateMask, וכך אפשר להצהיר שלא נופלים ל-EcoQoS ושבקשת timer resolution לא מתעלמים ממנה.
cm["ControlMask בוחר יעד שליטה"] --> sm["StateMask קובע פועל וכבוי"]
sm --> r1["הצהרה שלא נופלים ל-EcoQoS"]
sm --> r2["בקשת timer resolution לא מתעלמים ממנה"]
cm -.-> zero["אם ControlMask הוא 0, חוזרים ל-OS"]
איור 16: תפקיד שתי המסכות של SetProcessInformation. קודם בוחרים יעד שליטה, ואז מצהירים on / off.
כשקוראים מ-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>מבטל גם את הנטייה לחיסכון בחשמל, וגם את ההתעלמות מבקשת timer resolution.</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 נדרשת הרשאת PROCESS_SET_INFORMATION, אבל ה-handle המדומה של ה-process העצמי שמחזיר GetCurrentProcess() לא בעייתי.
4.6. שיבוץ CPU / מעבר בין cores / חום
בשיבוץ CPU, לרוב עדיף להתחיל מ-ציון רך הקרוב ל-soft affinity, “רוצים שהריצה תהיה בקבוצת ה-cores הזו”, ולא ישר מ-pin ל-core ספציפי (hard affinity / CPU pinning).
flowchart LR
accTitle: סדר הצעדים בשיבוץ cores
accDescr: קודם מודדים, אחר כך מנסים SetThreadIdealProcessor או CPU Sets, ורק אם השיפור אינו מספיק שוקלים בסוף SetThreadAffinityMask, תוך בדיקה מקבילה של טמפרטורה, clock וריצה ארוכה.
Measure["קודם מודדים"] --> Ideal["SetThreadIdealProcessor / CPU Sets"]
Ideal --> Check{"השתפר מספיק?"}
Check -->|"כן"| Keep["עוצרים כאן"]
Check -->|"לא"| Hard["בסוף שוקלים SetThreadAffinityMask"]
Measure --> Therm["באותו זמן בודקים גם טמפרטורה / clock / ריצה ארוכה"]
איור 17: שיבוץ CPU מתחיל במדידה. אם ציון רך מספיק, עוצרים שם. pin ל-core ספציפי הוא המוצא האחרון.
checklist
- קודם מודדים, ורק אז נוגעים בשיבוץ CPU
- לא עושים pin ישר ל-core ספציפי
- בודקים קודם את
SetThreadIdealProcessorאו CPU Sets SetThreadAffinityMaskנחשב מוצא אחרון- בריצה ארוכה בודקים טמפרטורה, clock ו-thermal throttling
- בודקים מצב שקט או רעש נמוך ב-laptop
הסדר הבטוח הוא הזרימה הזו.
- קודם מודדים
- במידת הצורך, ideal processor / CPU Sets
- אם עדיין נדרש שיפור, pin ל-core ספציפי
pin ל-core ספציפי נראה יעיל, אבל הוא מצמצם את דרכי המילוט של ה-OS, ולכן שימוש לא זהיר בו עלול להפוך את המצב לפחות גמיש.
4.7. isolation של driver / DPC / ISR / הפרעות חיצוניות
כשקורה “לפעמים רק ה-max מתפוצץ” או “הממוצע טוב אבל p99.9 גרוע”, כדאי לחשוד גם בהפרעות חיצוניות מחוץ לקוד האפליקציה.
flowchart TD
accTitle: עץ ההחלטה כשמופיע spike
accDescr: סדר isolation כשמופיע spike של late, miss או max: תחילה זמן העיבוד של האפליקציה, אחר כך DPC ו-ISR, אחר כך page fault, GC ועלות ראשונה, אחר כך סוללה, חיסכון בחשמל וחום, ולבסוף חפירה עם ETW, WPA או LatencyMon.
Spike["הופיע spike של late / miss / max"] --> Q1{"גם זמן העיבוד שלכם ארוך?"}
Q1 -->|"כן"| App["קיצור hot path / הפחתת allocations / הסרת I/O"]
Q1 -->|"לא"| Q2{"יש spike של DPC / ISR?"}
Q2 -->|"כן"| Driver["בודקים USB / Wi-Fi / Bluetooth / GPU / audio / storage / ACPI / עדכון driver"]
Q2 -->|"לא"| Q3{"יש page fault / GC / עלות ראשונה?"}
Q3 -->|"כן"| Mem["הקצאה מראש / warmup / הפחתת עומס ב-heap"]
Q3 -->|"לא"| Q4{"יש השפעה של סוללה / חיסכון בחשמל / חום?"}
Q4 -->|"כן"| Pow["AC / הגדרות power / קירור / בדיקה ארוכה"]
Q4 -->|"לא"| ETW["חפירה עם ETW / WPA / LatencyMon"]
איור 18: סדר ה-isolation כשמופיע spike. חושדים לפי הסדר בעיבוד שלכם עצמו, ב-DPC/ISR, בזיכרון, וב-power וחום.
checklist
- בודקים drivers סביב Wi-Fi / Bluetooth / USB / storage / GPU / audio
- מכבים סנכרון ענן, בניית אינדקס, ועדכון אוטומטי מיותרים ומשווים
- בודקים גם אם זה נשבר בהקטנה, או אם זה נשבר כשהמסך כבוי
- בודקים מגמות DPC / ISR עם LatencyMon או ETW
- מבדילים בין “העיבוד שלנו כבד” לבין “מישהו מבחוץ עוצר אותנו”
5. מדידה והערכה
5.1. מה מתעדים
אלה הדברים שכדאי לקחת לפחות.
- זמן מתוכנן של המחזור
- זמן התחלה בפועל
- זמן סיום בפועל
- lateness (כמה איחור ביחס להתחלה המתוכננת)
- זמן ריצה
- מספר deadline misses
- מספר deadline misses ברצף
- עומק queue
- מספר drop
- ניצול CPU
- הטיה בין cores
- DPC / ISR spikes
- page fault
- תנודות טמפרטורה / clock
גם אם מסתכלים רק על הממוצע, קשה לתפוס את המהות. מה שגורם לבעיות בייצור הוא ה-spikes הגדולים שמופיעים מדי פעם.
5.2. איך קוראים p99 / p99.9 / max
מדדים כמו p99 נועדו לראות את הזנב האיטי. בממוצע בלבד, ה-latency הגדול שמופיע מדי פעם מוסתר.
| מדד | משמעות | תמונה כשמודדים 10,000 פעמים |
|---|---|---|
| ממוצע | ערך ממוצע כללי | קל שה-spike ייטמע בו |
| p50 | הערך האמצעי | קרוב לתחושה היומיומית |
| p95 | הסף שבו מתחילים להיראות 5% האיטיים | הסף אחרי הוצאת 500 המדידות האיטיות |
| p99 | הסף שבו מתחיל להיראות 1% האיטי | הסף אחרי הוצאת 100 המדידות האיטיות |
| p99.9 | הסף שבו מתחיל להיראות 0.1% האיטי | הסף אחרי הוצאת 10 המדידות האיטיות |
| max | הערך הגרוע ביותר | המדידה האיטית ביותר מכולן |
לדוגמה, נניח שהתקבל הרצף הבא (דוגמה להסבר איך קוראים את המדדים, לא ערכים שנמדדו בפועל מציוד מסוים).
- ממוצע: 0.8ms
- p99: 1.2ms
- p99.9: 3.5ms
- max: 28ms
זה מצב שבו בדרך כלל מהיר, אבל מדי פעם יש spike גדול. ב-Windows רגיל, הבעיה האמיתית מופיעה בדרך כלל בזנב הזה, מ-p99 עד max.
flowchart TB
accTitle: ההבדל בין הממוצע לבין המדדים בצד הזנב
accDescr: כשמסתכלים רק על הממוצע, ה-latency הגדול שמופיע מדי פעם מוסתר. כשמסתכלים על p99, p99.9 ו-max, נראה הזנב האיטי, וב-Windows רגיל הבעיה האמיתית מופיעה שם.
avg["מסתכלים רק על הממוצע"] --> hide["ה-latency הגדול שמופיע מדי פעם מוסתר"]
tail["מסתכלים על p99, p99.9 ו-max"] --> see["נראה הזנב האיטי"]
see --> real["הבעיה האמיתית מופיעה בין p99 ל-max"]
איור 19: גם אם הממוצע טוב, אם ה-max קופץ, זה מצב “בדרך כלל מהיר אבל קופץ מדי פעם”. תופסים את זה עם מדדי הזנב.
המאמר הזה אינו מציג ערכים מספריים להשפעת המבנה המומלץ. latency ו-jitter משתנים בקלות לפי CPU, driver, תוכנה נלווית, הגדרות power ואופן העמסת העומס, ולכן ערכים ממחשב של מישהו אחר לא משמשים כשלעצמם הוכחה עבור הסביבה שלכם. במקום זאת, הנה נוהל ליצירת אותה טבלה עבור הסביבה שלכם.
הנוהל המינימלי להוצאת p99 בסביבה שלכם
- ב-hot path מתעדים בלבד. לוקחים lateness וזמן ריצה עם
Stopwatch.GetTimestamp()(ב-C++:QueryPerformanceCounter), וכותבים למערך שהוקצה מראש. אסור לחשב כאן ממוצע או מיון - עוצרים את המדידה, ורק אז מרכזים. ממיינים, ומוציאים את הערך במיקום האחוזון
- חוזרים על אותו נוהל בשינוי תנאים: לפני/אחרי warmup, AC/סוללה, UI קדמי/ממוזער, עם/בלי עומס מ-processes אחרים (5.4)
- בכל שינוי בודד, לוקחים מדידה חדשה באותם תנאים ומשווים
// C# / .NET 8. הריכוז מתבצע אחרי שעוצרים את המדידה
using System.Diagnostics;
// ב-hot path רק כותבים למערך (אפס allocations)
long[] latenessTicks = new long[100_000];
int count = 0;
// דוגמה: בתוך ה-periodic loop
// 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 מזהים חשוד בתנודה שמקורה ב-driver
- ניטור טמפרטורה / clock בודקים את השפעת החום
flowchart LR
accTitle: מה מודדים ואיך מחליטים
accDescr: מדידה פנימית באפליקציה נותנת התפלגות אחוזונים ומדדי miss, drop ועומק queue. ETW, WPR ו-WPA מגלים context switch, DPC, ISR ו-page fault. ניטור טמפרטורה ו-clock מצטרף אליהם כדי לקבוע סדר עדיפות לשיפור.
App["מדידה פנימית באפליקציה"] --> Dist["p50 / p95 / p99 / p99.9 / max"]
App --> Miss["miss / drop / עומק queue"]
ETW["ETW / WPR / WPA"] --> Root["context switch / DPC / ISR / page fault"]
Temp["ניטור טמפרטורה / clock"] --> 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 |
פותחים שורת פקודה כ-Administrator, (1) מתחילים הקלטה עם wpr -start CPU, (2) מריצים את העיבוד הבעייתי כמה עשרות שניות, (3) שומרים עם wpr -stop trace.etl "חקירת latency מחזורי". אחר כך פותחים את trace.etl ב-WPA. שמות הפרופילים הזמינים ניתן לבדוק עם wpr -profiles |
| LatencyMon | מורידים מהאתר של Resplendence Software. גרסת Home Edition לשימוש אישי היא בחינם | מפעילים, מתחילים מדידה, ומריצים את העיבוד הרלוונטי כמה דקות. מתקבל ריכוז של השהיית ה-timer המקסימלית של ה-kernel, וגם זמני ISR / DPC לפי driver, ו-hard pagefault, כך שרושמים את ה-drivers שזמן הריצה שלהם בולט |
מה שמסתכלים עליו קודם ב-WPA, מבין הגרפים הקשורים ל-CPU, הם זמן הריצה של DPC / ISR ו-context switch. מוצג “איזה driver החזיק את ה-CPU בזמן שה-thread שלכם לא יכול היה לרוץ”, ומשווים את זה לזמני ה-latency שתועדו ב-5.1.
המאמר הזה לא כולל צילומי מסך. המראה משתנה הרבה בין גרסאות, לכן מבצעים את השלבים למעלה ובודקים במסך האמיתי.
5.4. איך בודקים
בדיקה בסביבת benchmark שקטה בלבד לא מספיקה. כדאי לפחות לבדוק בנפרד את התנאים האלה.
- לפני warmup, מיד אחרי startup
- אחרי warmup
- ריצה ארוכה ורציפה
- UI קדמי
- מצב קרוב ל-UI ממוזער / מוסתר
- AC
- סוללה
- מצב עם עומס על רשת או דיסק
הערכה רק בסביבת benchmark נוטה לפספס בעיות שמופיעות בתפעול בפועל. מכיוון ש-ההתנהגות של Windows רגיל נמשכת אחרי אופן השימוש, חשוב לבדוק תוך התקרבות לתנאים בפועל.
flowchart TB
accTitle: למה בודקים בתנאים נפרדים
accDescr: הערכה רק בסביבת benchmark שקטה מפספסת בעיות מהתפעול בפועל, ולכן בודקים בנפרד תנאים כמו לפני/אחרי warmup וריצה ארוכה, UI קדמי מול ממוזער, ו-AC מול סוללה ועומס.
bench["הערכה רק בסביבת benchmark שקטה"] -.-> lose["מפספסים בעיות מהתפעול בפועל"]
lose -.-> split["בודקים בתנאים נפרדים"]
split --> c1["warmup וריצה ארוכה"]
split --> c2["UI קדמי וממוזער"]
split --> c3["תנאי חיבור לחשמל ועומס"]
איור 21: ההתנהגות של Windows רגיל נמשכת אחרי אופן השימוש. מעריכים תוך התקרבות לתנאי השימוש בפועל.
6. איך בוחרים בפועל
טבלת הקריאה המהירה לפי טווח מחזור הועברה ל-סעיף 1.1 כדי שתוכלו לחזור אליה תוך כדי קריאה. כאן משלימים שתי החלטות שהטבלה הזו לבדה לא קובעת.
מתי מחליטים ש”אי אפשר עם Windows רגיל”
כשמופיעה דרישה לשמור על פחות מ-1ms לאורך זמן ובעומס גבוה, לרוב מהיר יותר להחליט להעביר החוצה רק את החלק הרגיש לזמן, מאשר להמשיך לכוונן. מועמדי היעד הם firmware בצד ה-device, בקר ייעודי, FPGA, RTOS. חומר ההחלטה צריך להיות לא סובייקטיבי אלא המספרים מפרק 5.
- למרות שאי אפשר לקצר עוד את ה-hot path, p99.9 וה-max ממשיכים לחרוג מהדרישה
- הגורם הוא הפרעה חיצונית (DPC / ISR, driver, process אחר), ואמצעים בצד האפליקציה לא מספיקים (מאושר לפי ה-isolation בסעיף 4.7)
- הדרישה היא לא “לא נשברים גם כשמפספסים מדי פעם” אלא “אסור לפספס אפילו פעם אחת”
כשרוצים לחיות עם הכול ב-process אחד
כשמחזיקים GUI, logging, תקשורת ו-DB באותה לולאה של אותו process, העבודה בשלב המאוחר שוברת את ה-deadline של השלב המוקדם. המתנה ל-flush של קובץ, חיבור מחדש ל-DB, וציור מחדש של UI, כולם יכולים להתארך בסדר גודל של עשרות ms. הרחבת ההפרדה בין fast path ל-slow path (4.2) עד לרמת ה-process, ולא רק ה-thread, היא גם אפשרות. כשמפרידים ל-processes, גם אם השלב המאוחר נתקע, המחזור של השלב המוקדם ממשיך לרוץ.
flowchart TB
accTitle: השוואה בין מבנה משותף להפרדת processes
accDescr: כשמחזיקים GUI, logging, תקשורת ו-DB באותה לולאה של אותו process, העבודה בשלב המאוחר שוברת את ה-deadline של השלב המוקדם. הרחבת הפרדת fast path/slow path לרמת ה-process שומרת על המחזור המוקדם גם כשהשלב המאוחר נתקע.
one["מחזיקים הכול ב-process אחד ולולאה אחת"] --> broke["השלב המאוחר שובר את ה-deadline המוקדם"]
broke -.-> ex["המתנה ל-flush, חיבור מחדש, ציור מחדש"]
sep["מפרידים בין השלב המוקדם למאוחר לפי process"] --> keep["גם אם השלב המאוחר נתקע, המוקדם ממשיך לרוץ"]
איור 22: אם באמת נדרש לחיות יחד, יש אפשרות להרחיב את הפרדת fast path / slow path עד לרמת ה-process.
7. סיכום
יש שתי הנחות שכדאי לזכור.
- ב-Windows רגיל היעד אינו hard real-time. היעד הוא soft real-time: מקטינים latency ו-jitter, ולא נשברים גם כשמפספסים deadline
- מה שהכי משפיע הוא סידור ה-hot path, יותר מכיוונון priority
מבחינת מימוש, מה שעוזר הוא בערך זה.
- מפרידים בין fast path ל-slow path
- queue באורך קבוע, ומדיניות שהוחלטה מראש למקרה שהוא עולה על גדותיו
- מודדים עם QPC, וממתינים עם event / waitable timer
- נמנעים ב-hot path מ-allocation, I/O חוסם, ו-locks כבדים
מבחינת תפעול, מה שעוזר הוא בערך זה.
- מריצים על AC
- מפרידים הגדרות power לייצור
- מפחיתים עומס רקע מיותר
- מעריכים לפי p99 / p99.9 / max ומספר misses
soft real-time ב-Windows רגיל לא נקבע רק על ידי הגדרת priority. אם מטפלים בנפרד בתכנון, במימוש, בהגדרות power, במדידה ובתפעול, אפשר להגיע למערכת די יציבה.
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
מאמרים קשורים
מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.
איך קיצור דרך ב-Windows מוצא קובץ שהעברתם? — המיקום של הקובץ והזהות שלו הם שני דברים שונים
העברתם את הקובץ המקורי, ובכל זאת הקיצור פותח אותו. Windows מחפש את היעד לא רק לפי הנתיב השמור, אלא גם לפי מזהי מעקב ומאפיינים של הקובץ, ו...
האם עדיין צריך "הסרה בטוחה" של כונן USB? — מבט מ"הסרה מהירה" ומטמון הכתיבה
אפשר לשלוף כונן USB ברגע שההעתקה הסתיימה? ההבדל בין מטמון הכתיבה, "הסרה מהירה" ו"ביצועים גבוהים" מסביר מה תפקידה של ההסרה הבטוחה. המאמר כ...
למה האודיו נקטע כשה-CPU לא עסוק? — מבט דרך buffer ו-deadline
האודיו נקטע בזמן שהשימוש ב-CPU נמוך. ההסבר יוצא מה-buffer של הניגון ומה-deadline של המילוי מחדש, ממשיך למה buffer גדול יותר מוסיף השהיה, ...
למה RDP איטי למרות חיבור מהיר? — להפריד בין input, rendering ורשת
בדיקת המהירות מראה מהירות גבוהה, ובכל זאת ה-input וה-scroll ב-Remote Desktop מאחרים. ההסבר עובר מ-round trips והעברת המסך, דרך השוואה לפי...
הרשת עובדת אבל Windows מציג "אין אינטרנט" — לבודד NCSI, DNS, Proxy ו-VPN ב-Windows
למה Windows מציג "אין אינטרנט" בזמן שהרשת עובדת — נקודת המוצא היא קביעת הקישוריות של NCSI. כאן מפרידים בין DNS, proxy, VPN ו-captive port...
נושאים קשורים
העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.
נושאים טכניים ב-Windows
שער לנושאי פיתוח Windows, חקירת תקלות וניצול נכסים קיימים.
שירותים הקשורים לנושא הזה
המאמר קשור ישירות לשירותים הבאים.
פיתוח יישומי Windows
יישומים עסקיים, חיבור התקנים וכלי תקשורת, מהגדרת הדרישות ועד הפיתוח.
שאלות נפוצות
שאלות נפוצות בפניות בנושא המאמר.
- אפשר לעשות real-time processing ב-Windows?
- אי אפשר להבטיח hard real-time (אפס deadline misses). אבל אם מתכננים, מממשים, מודדים ומפעילים כמו שצריך, אפשר להגיע למצב די מעשי כ-soft real-time. periodic processing של כמה ms עד כמה עשרות ms, audio/video שמבוססים על buffer, קריאה מחיישנים ו-control loops — כל אלה ריאליים גם ב-Windows 10/11 רגיל. אם לעומת זאת צריך אפס deadline misses, או יציבות לאורך זמן מתחת למאות מיקרו-שניות, כדאי להעביר את זה ל-RTOS, בקר ייעודי, FPGA, או עיבוד בצד ה-device.
- למה אסור לבסס periodic loop על Sleep(1)?
- כי Sleep(1) אינו "מחזור של 1ms". הוא ממתין בערך 1ms ומעלה, ואז מוסיפים מעל זה את זמן העיבוד. overshoot של ההמתנה מצטבר כמו שהוא. periodic loop יציב יותר כשהוא רץ לפי absolute deadline עם next += period; להמתנה מעדיפים device event או waitable timer ברזולוציה גבוהה, ורק את ה-fine-tune האחרון מגבילים ל-busy-spin קצר מאוד. את timeBeginPeriod קוראים רק כל עוד צריך, ואחר כך מחזירים.
- מה הכי עוזר להקטין latency ו-jitter?
- לא להעלות priority. לקצר את ה-hot path, להפוך אותו ל-fixed-size, ולשמור אותו non-blocking. מפרידים fast path (acquisition ובקרה) מ-slow path (שמירה, תקשורת, UI) ומחברים ביניהם ב-queue באורך קבוע. ב-hot path נמנעים מכתיבה לקובץ, שליחה ברשת, logging כבד, ו-allocation בכל איטרציה. בצד התפעול: AC power, בדיקת power mode, EcoQoS, וצמצום עומס ברקע.
- באילו מדדים מעריכים יציבות של periodic processing?
- לא רק בממוצע. מסתכלים על p99, p99.9, max, ומספר ה-deadline misses. למשל ממוצע 0.8ms עם max של 28ms אומר: בדרך כלל מהיר, אבל מדי פעם יש spike גדול. ב-Windows רגיל הבעיה האמיתית יושבת בזנב, מ-p99 עד max. במקביל מתעדים גם DPC/ISR spikes, page fault, עומק queue, ותנודות טמפרטורה/clock, ומעריכים בנפרד warmup לפני/אחרי, ריצה ארוכה, מצב ממוזער, וסוללה.