לדלג לתוכן

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:

add [ebx+ecx*4], eax

פירוק למיקרו-פעולות:
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.