לדלג לתוכן

8.1 צינור ההוראות הרצאה

הקדמה

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

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


צינור ההוראות הקלאסי - the classic RISC pipeline

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

במעבדי RISC קלאסיים, כל הוראה עוברת 5 שלבים:

שלב שם מקוצר שם מלא מה קורה
1 IF Instruction Fetch המעבד קורא את ההוראה מהזכרון (או מהcache) לפי הכתובת שבIP
2 ID Instruction Decode המעבד מפענח את ההוראה - מזהה איזו פעולה צריך לבצע ומאילו אוגרים לקרוא
3 EX Execute יחידת הALU מבצעת את החישוב (חיבור, חיסור, השוואה, חישוב כתובת)
4 MEM Memory Access אם ההוראה דורשת גישה לזכרון (load/store), הגישה מתבצעת כאן
5 WB Write Back התוצאה נכתבת חזרה לאוגר היעד

בלי צינור, כל הוראה לוקחת 5 מחזורי שעון (cycles), וההוראה הבאה מתחילה רק אחרי שהקודמת סיימה. עם צינור, ברגע שהוראה מסיימת את שלב הIF ועוברת לID, הוראה חדשה נכנסת לשלב IF. כך בכל רגע נתון יש עד 5 הוראות שונות בתוך הצינור.


דיאגרמת צינור - pipeline diagram

הנה דוגמה של 5 הוראות שזורמות בצינור. כל עמודה היא מחזור שעון אחד:

           מחזור:  1     2     3     4     5     6     7     8     9
הוראה 1:         [IF]  [ID]  [EX]  [MEM] [WB]
הוראה 2:               [IF]  [ID]  [EX]  [MEM] [WB]
הוראה 3:                     [IF]  [ID]  [EX]  [MEM] [WB]
הוראה 4:                           [IF]  [ID]  [EX]  [MEM] [WB]
הוראה 5:                                 [IF]  [ID]  [EX]  [MEM] [WB]

שימו לב למה שקורה כאן:
- במחזור 5, כל 5 השלבים של הצינור עובדים בו-זמנית - הוראה 1 ב-WB, הוראה 2 ב-MEM, הוראה 3 ב-EX, הוראה 4 ב-ID, הוראה 5 ב-IF.
- בלי צינור, 5 הוראות היו לוקחות 5 x 5 = 25 מחזורים. עם צינור, לקח רק 9 מחזורים.


תפוקה מול חביון - throughput vs latency

חשוב להבין נקודה מהותית: הצינור לא מאיץ הוראה בודדת. הוראה אחת עדיין לוקחת 5 מחזורים מתחילתה ועד סופה (זה החביון - latency). מה שהצינור משפר הוא את התפוקה - throughput - כמה הוראות מסתיימות ביחידת זמן.

כשהצינור מלא ועובד בצורה חלקה, הוראה אחת מסתיימת בכל מחזור שעון. כלומר:
- חביון (latency): 5 מחזורים להוראה אחת (לא השתנה)
- תפוקה (throughput): הוראה אחת למחזור (שיפור פי 5!)

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


סכנות בצינור - pipeline hazards

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

סכנת נתונים - data hazard

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

add eax, ebx    ; הוראה 1: eax = eax + ebx
sub ecx, eax    ; הוראה 2: ecx = ecx - eax (צריכה את eax החדש!)

הוראה 2 צריכה לקרוא את eax, אבל הוראה 1 תכתוב את התוצאה ל-eax רק בשלב WB (מחזור 5). הוראה 2 מגיעה לשלב ID (קריאת אוגרים) כבר במחזור 3 - שני מחזורים מוקדם מדי!

           מחזור:  1     2     3     4     5
add eax, ebx:    [IF]  [ID]  [EX]  [MEM] [WB] <-- כותב eax כאן
sub ecx, eax:          [IF]  [ID]  ...         <-- צריך eax כאן!
                                                   eax עדיין לא מוכן

פתרון 1 - העברה - forwarding (bypassing):

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

           מחזור:  1     2     3     4     5     6
add eax, ebx:    [IF]  [ID]  [EX]  [MEM] [WB]
                               |
                     forwarding|  (התוצאה מועברת ישירות)
                               v
sub ecx, eax:          [IF]  [ID]  [EX]  [MEM] [WB]

בזכות forwarding, הוראה 2 מקבלת את הערך החדש של eax בזמן - בלי עיכוב.

פתרון 2 - השהייה - stalling (bubbles):

כשforwarding לא מספיק (למשל, כשהוראה 1 היא load מזכרון - התוצאה מוכנה רק אחרי שלב MEM), המעבד מכניס "בועות" - מחזורים ריקים שבהם הצינור עוצר ומחכה:

           מחזור:  1     2     3     4     5     6     7
ldr eax, [mem]:  [IF]  [ID]  [EX]  [MEM] [WB]
                                     |
                                     | (התוצאה מוכנה רק כאן)
                                     v
sub ecx, eax:          [IF]  [ID]  [--]  [EX]  [MEM] [WB]
                                   בועה!

הבועה (bubble) היא מחזור מבוזבז - הצינור עוצר לרגע. זו הסיבה שload מזכרון ואחריו שימוש מיידי בתוצאה יוצר עיכוב.


סכנת בקרה - control hazard

סכנת בקרה קורית כשיש הוראת הסתעפות (branch) - כמו jz, jne, jmp - שמשנה את זרימת התוכנית. הבעיה: בזמן שהוראת ההסתעפות עדיין נמצאת בשלב ID או EX (וטרם חושב התנאי), הצינור כבר הספיק לטעון הוראות מהכתובת הבאה. אם ההסתעפות כן מתקיימת, ההוראות שנטענו הן שגויות!

cmp eax, 0
jz  label       ; אם eax == 0, קפוץ ל-label
add ebx, 1      ; נטען לצינור - אבל אולי לא צריך לרוץ!
mov ecx, 5      ; גם זה נטען מוקדם מדי
label:
sub edx, 1

פתרון 1 - חיזוי הסתעפויות - branch prediction:

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

פתרון 2 - שטיפת צינור - pipeline flush:

אם הניחוש היה שגוי, המעבד צריך לשטוף את כל ההוראות השגויות מהצינור ולהתחיל מחדש מהכתובת הנכונה. כל העבודה שנעשתה על ההוראות השגויות הולכת לפח. בצינור של 5 שלבים זה אומר אובדן של 2-3 מחזורים. בצינור עמוק של 15-20 שלבים, זה אובדן של 15-20 מחזורים - מחיר כבד מאוד!


סכנת מבנה - structural hazard

סכנת מבנה קורית כששתי הוראות צריכות את אותו רכיב חומרה בו-זמנית. דוגמה קלאסית: הוראה אחת נמצאת בשלב IF (צריכה לקרוא הוראה מהזכרון) בזמן שהוראה אחרת בשלב MEM (צריכה לקרוא/לכתוב נתונים מהזכרון). אם יש רק יציאה אחת לזכרון, הן לא יכולות לעבוד בו-זמנית.

פתרון: שכפול חומרה

הפתרון הנפוץ הוא לשכפל את הרכיב הבעייתי. למשל:
- מטמון הוראות ומטמון נתונים נפרדים (L1I ו-L1D) - כך שלב IF קורא ממטמון אחד ושלב MEM קורא ממטמון אחר, ללא התנגשות
- יציאות קריאה מרובות בregister file - מאפשרות לכמה הוראות לקרוא אוגרים בו-זמנית
- יחידות חישוב מרובות - כמה ALU-ים, כך שכמה הוראות יכולות להיות בשלב EX בו-זמנית (נושא שנרחיב בפרק 8.4 על ביצוע superscalar)


מעבדי x86 מודרניים - צינור עמוק

הצינור של 5 שלבים שראינו הוא מודל פשוט ומוקדם. מעבדי x86 מודרניים של Intel ו-AMD משתמשים בצינורות הרבה יותר עמוקים:

מעבד שנה (בערך) עומק צינור
Intel Pentium 1993 5 שלבים
Intel Pentium III 1999 10 שלבים
Intel Pentium 4 (Northwood) 2002 20 שלבים
Intel Pentium 4 (Prescott) 2004 31 שלבים!
Intel Core (Skylake) 2015 14-19 שלבים
AMD Zen 4 2022 19 שלבים

למה צינור עמוק יותר? כי כל שלב עושה פחות עבודה, מה שמאפשר תדר שעון (clock frequency) גבוה יותר. ה-Pentium 4 עם 31 שלבים הגיע ל-3.8 GHz - תדר גבוה מאוד לתקופתו.

אבל יש מחיר: ככל שהצינור עמוק יותר, המחיר של branch misprediction גדל (יותר הוראות לזרוק בshutdown), ויותר מקומות שבהם יכולים להיווצר hazards. ב-Pentium 4 Prescott עם 31 שלבים, branch misprediction עלתה 31 מחזורים! Intel למד את הלקח וחזר לצינורות קצרים יותר (14-19 שלבים) בארכיטקטורת Core.


CISC מול RISC

RISC - Reduced Instruction Set Computer

ארכיטקטורות כמו ARM ו-RISC-V הן ארכיטקטורות RISC:
- הוראות באורך קבוע (32 ביט בARM)
- מספר קטן של צורות הוראה פשוטות
- רק הוראות load/store ניגשות לזכרון - פעולות חישוב עובדות רק על אוגרים
- קל לפענח, קל לבנות צינור יעיל

CISC - Complex Instruction Set Computer

ארכיטקטורת x86 היא CISC:
- הוראות באורך משתנה (1 עד 15 בתים ב-x86!)
- הוראות מורכבות שיכולות לבצע כמה פעולות (למשל add eax, [ebx+ecx*4+8] שקוראת מזכרון ומחברת במכה אחת)
- מצבי כתובת (addressing modes) רבים ומורכבים

הסוד של x86 מודרני - פירוק מיקרו - micro-op decomposition

כאן מגיע החלק המעניין באמת. מעבדי x86 מודרניים (מאז ה-Pentium Pro ב-1995) הם RISC מבפנים. בשלב הדקוד, המעבד מפרק כל הוראת CISC מורכבת לסדרה של מיקרו-פעולות - micro-ops (uops) - הוראות פנימיות פשוטות בדומה להוראות RISC.

לדוגמה, ההוראה:

add eax, [ebx+ecx*4+8]

מפורקת בתוך המעבד לכמה uops:
1. חישוב הכתובת: ebx + ecx*4 + 8
2. קריאה מהזכרון: load temp, [address]
3. חיבור: add eax, temp

ה-uops הפנימיים זורמים בצינור הפנימי בדיוק כמו הוראות RISC - אורך קבוע, פשוטים, קלים לתזמן. כך מעבדי x86 נהנים משני העולמות:
- תאימות לאחור - אפשר להריץ את אותו קוד x86 מאז 1978
- ביצועים גבוהים - הליבה הפנימית היא RISC-like עם צינור יעיל

זו הסיבה שכשנדבר על ביצועים של מעבדי x86 מודרניים, לעתים קרובות נדבר על uops ולא על הוראות x86 - כי ה-uops הם מה שבאמת זורם בצינור.


סיכום

מושג הסבר
צינור הוראות - pipeline חפיפה בין שלבי ביצוע של הוראות שונות
תפוקה - throughput כמה הוראות מסתיימות ביחידת זמן (משתפרת)
חביון - latency כמה זמן לוקח להוראה אחת (לא משתנה)
סכנת נתונים - data hazard תלות בתוצאה של הוראה קודמת
העברה - forwarding קיצור דרך בחומרה לתוצאות
סכנת בקרה - control hazard הסתעפות שמשנה את זרימת ההוראות
שטיפת צינור - pipeline flush זריקת הוראות שנטענו בטעות
סכנת מבנה - structural hazard שני שימושים באותו רכיב חומרה
מיקרו-פעולות - micro-ops הוראות RISC פנימיות שx86 מפורק אליהן

בפרק הבא (8.2) נעמיק בחיזוי הסתעפויות - אחד המנגנונים הקריטיים ביותר בביצועי מעבדים מודרניים.