8.1 צינור ההוראות פתרון
פתרון תרגול - צינור ההוראות¶
1. ציור דיאגרמת צינור¶
סעיף 1 - בלי hazards:
מחזור: 1 2 3 4 5 6 7 8
mov eax, 10: [IF] [ID] [EX] [MEM] [WB]
mov ebx, 20: [IF] [ID] [EX] [MEM] [WB]
add ecx, eax: [IF] [ID] [EX] [MEM] [WB]
sub edx, ebx: [IF] [ID] [EX] [MEM] [WB]
כל 4 ההוראות מסתיימות ב-8 מחזורים (4 + 5 - 1 = 8).
סעיף 2 - עם data hazard ו-forwarding מ-EX ל-EX:
הוראה 1 (mov eax, 10) מחשבת את הערך בשלב EX (מחזור 3). הוראה 3 (add ecx, eax) צריכה את eax בשלב EX (מחזור 5). בזכות forwarding מפלט EX של הוראה 1 (מחזור 3) - אבל הוראה 3 מגיעה ל-EX רק במחזור 5, שזה אחרי מחזור 3. בפועל, כשהוראה 3 מגיעה ל-EX, הערך כבר עבר שלבים נוספים ובודאי זמין. אין עיכוב.
מחזור: 1 2 3 4 5 6 7 8
mov eax, 10: [IF] [ID] [EX] [MEM] [WB]
mov ebx, 20: [IF] [ID] [EX] [MEM] [WB]
add ecx, eax: [IF] [ID] [EX] [MEM] [WB]
sub edx, ebx: [IF] [ID] [EX] [MEM] [WB]
הדיאגרמה זהה - אין stall כי יש הוראה אחת (mov ebx, 20) בין הוראה 1 לבין הוראה 3, ויש מספיק זמן ל-forwarding.
סעיף 3 - עם load מזכרון:
כשהוראה 1 היא mov eax, [mem] (load), התוצאה מוכנה רק בסוף שלב MEM (מחזור 4). אם הוראה 3 מגיעה ל-EX במחזור 5, היא יכולה לקבל forwarding מפלט MEM של הוראה 1 (מחזור 4). שוב, בזכות הוראה 2 שביניהן, יש מספיק "רווח". אין עיכוב גם כאן.
אבל - אילו הוראה 3 הייתה מיד אחרי הוראה 1 (בלי הוראה 2 באמצע):
מחזור: 1 2 3 4 5 6
mov eax, [mem]: [IF] [ID] [EX] [MEM] [WB]
add ecx, eax: [IF] [ID] [--] [EX] [MEM] [WB]
בועה!
היינו צריכים stall אחד (בועה אחת) כי ה-load מסיים MEM במחזור 4, ו-add צריך את הערך ב-EX שהיה מתוכנן למחזור 4 - אין מספיק זמן. אז ה-EX של add נדחה למחזור 5.
2. זיהוי סכנות¶
mov eax, [ebx] ; (1)
add eax, ecx ; (2)
mov [edx], eax ; (3)
sub ecx, 5 ; (4)
cmp ecx, 0 ; (5)
jz done ; (6)
סכנות נתונים:
| סכנה | הוראות | אוגר | סוג |
|---|---|---|---|
| 1 | (1) -> (2) | eax | RAW (Read After Write) - הוראה 2 קוראת eax שהוראה 1 כותבת |
| 2 | (2) -> (3) | eax | RAW - הוראה 3 קוראת eax שהוראה 2 כותבת |
| 3 | (4) -> (5) | ecx | RAW - הוראה 5 קוראת ecx שהוראה 4 כותבת |
Forwarding מול Stall:
- סכנה 1 ((1) -> (2)): הוראה 1 היא load מזכרון. התוצאה מוכנה רק אחרי MEM. הוראה 2 היא מיד אחריה. נדרש stall אחד (load-use hazard). Forwarding מ-MEM ל-EX + בועה אחת.
- סכנה 2 ((2) -> (3)): הוראה 2 מחשבת eax ב-EX. הוראה 3 צריכה eax ב-EX. בזכות stall מסכנה 1, יש רווח. forwarding מספיק.
- סכנה 3 ((4) -> (5)): הוראה 4 מחשבת ecx ב-EX. הוראה 5 צריכה ecx ב-EX (מיד אחריה). forwarding מ-EX ל-EX מספיק.
סכנת בקרה:
הוראה 6 (jz) היא הסתעפות מותנית. בזמן שהjz נמצא בשלב EX (מחושב אם לקפוץ), הוראות שאחרי jz כבר נכנסו לצינור - הוראה שאחרי jz נמצאת ב-ID, וההוראה שאחריה ב-IF. אלו שתי הוראות שייתכן שנטענו בטעות.
אם jz קופץ:
בצינור 5 שלבים, ההסתעפות מתגלה בשלב EX (מחזור 3 של ההוראה). עד אז נכנסו 2 הוראות לצינור. Pipeline flush זורק אותן - 2 מחזורים הולכים לאיבוד.
3. חישוב תפוקה¶
סעיף 1 - בלי hazards, צינור בן 5 שלבים:
- מספר מחזורים = 5 + (100 - 1) = 104 מחזורים
(5 מחזורים להוראה הראשונה, ואחריה כל הוראה נוספת לוקחת מחזור אחד)
- תפוקה = 100 / 104 = 0.96 הוראות למחזור (כמעט 1 IPC)
- בלי צינור: 100 x 5 = 500 מחזורים, תפוקה = 100/500 = 0.2 IPC
סעיף 2 - branch כל 5 הוראות עם flush מלא (5 שלבים):
- יש 100/5 = 20 branches
- כל branch גורם ל-5 מחזורים מבוזבזים (flush מלא של צינור 5 שלבים)
- סה"כ: 104 + 20 x 5 = 104 + 100 = 204 מחזורים
- תפוקה = 100 / 204 = 0.49 IPC
סעיף 3 - צינור 20 שלבים, branch כל 5 הוראות:
- בלי hazards: 20 + (100 - 1) = 119 מחזורים
- flush מלא = 20 מחזורים מבוזבזים (לא 5!)
- סה"כ: 119 + 20 x 20 = 119 + 400 = 519 מחזורים
- תפוקה = 100 / 519 = 0.19 IPC
המסקנה ברורה: צינור עמוק סובל הרבה יותר מ-branch mispredictions. זו הסיבה שחיזוי הסתעפויות (פרק 8.2) קריטי כל כך במעבדים עם צינורות עמוקים.
4. CISC לעומת RISC ומיקרו-פעולות¶
סעיף 1 - פירוק ל-uops:
פירוק למיקרו-פעולות:
1. lea temp1, [ebx+ecx*4] - חישוב כתובת
2. load temp2, [temp1] - קריאה מהכתובת
3. add temp2, temp2, eax - חיבור
4. store [temp1], temp2 - כתיבה חזרה
סה"כ: 4 uops.
סעיף 2 - דיאגרמת צינור ל-uops:
מחזור: 1 2 3 4 5 6 7 8 9
lea temp1: [IF] [ID] [EX] [MEM] [WB]
load temp2: [IF] [ID] [EX] [MEM] [WB]
fwd|
add temp2: [IF] [ID] [--] [EX] [MEM] [WB]
בועה (load-use)
store: [IF] [ID] [--] [EX] [MEM] [WB]
ה-load מסיים את ערכו ב-MEM (מחזור 6), וה-add צריך אותו ב-EX - נדרשת בועה אחת. סה"כ: 9 מחזורים להוראת x86 אחת.
סעיף 3 - גרסת ARM (RISC):
מחזור: 1 2 3 4 5 6 7 8
ldr r4, [...]: [IF] [ID] [EX] [MEM] [WB]
fwd|
add r4, r4, r0: [IF] [ID] [--] [EX] [MEM] [WB]
בועה (load-use)
str r4, [...]: [IF] [ID] [EX] [MEM] [WB]
סה"כ: 8 מחזורים (בועה אחת בגלל load-use). זה דומה מאוד לתוצאה של ה-uops!
זה לא מפתיע - מעבד x86 בפנים עושה בדיוק את אותו הדבר.
סעיף 4 - מה היתרון של CISC אם בפנים זה RISC?
היתרון העיקרי הוא צפיפות קוד (code density). הוראת CISC אחת מכילה את כל המידע של 3-4 הוראות RISC. זה אומר:
- פחות בתים בזכרון לאותה תוכנית - חשוב ל-instruction cache (פרק 8.3)
- פחות fetches - שלב IF צריך לקרוא פחות הוראות
- תאימות לאחור - כל קוד x86 שנכתב מאז 1978 עדיין רץ על מעבדים חדשים
החיסרון: שלב הדקוד במעבדי x86 מורכב ואנרגטי יותר בגלל הצורך לפרק הוראות באורך משתנה ל-uops.