8.1 - צינור ההוראות - הרצאה
הקדמה¶
בפרק 0.2 למדנו את הבסיס - המעבד קורא הוראות מהזכרון ומבצע אותן אחת אחרי השנייה. הוראת MOV, אחריה הוראת ADD, אחריה הוראת CMP, וכך הלאה. התיאור הזה נכון מבחינה לוגית, אבל מעבדים מודרניים עובדים בצורה הרבה יותר מורכבת.
אם המעבד היה באמת מסיים הוראה אחת לגמרי לפני שמתחיל את הבאה, הוא היה מבזבז המון זמן. חלקים שונים של המעבד היו יושבים בטל בזמן שחלק אחד עובד. הפתרון? צינור הוראות - instruction pipeline - מנגנון שמאפשר למעבד לעבוד על מספר הוראות בו-זמנית, כל אחת בשלב שונה של הביצוע.
צינור ההוראות הקלאסי - the classic RISC pipeline¶
הרעיון של ה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 | התוצאה נכתבת חזרה לאוגר היעד |
בלי pipeline, כל הוראה לוקחת 5 מחזורי שעון (cycles), וההוראה הבאה מתחילה רק אחרי שהקודמת סיימה. עם pipeline, ברגע שהוראה מסיימת את שלב הIF ועוברת לID, הוראה חדשה נכנסת לשלב IF. כך בכל רגע נתון יש עד 5 הוראות שונות בתוך הpipeline.
דיאגרמת צינור - pipeline diagram¶
הנה דוגמה של 5 הוראות שזורמות בpipeline. כל עמודה היא מחזור שעון אחד:
Cycle: 1 2 3 4 5 6 7 8 9
Instr 1: [IF] [ID] [EX] [MEM] [WB]
Instr 2: [IF] [ID] [EX] [MEM] [WB]
Instr 3: [IF] [ID] [EX] [MEM] [WB]
Instr 4: [IF] [ID] [EX] [MEM] [WB]
Instr 5: [IF] [ID] [EX] [MEM] [WB]
שימו לב למה שקורה כאן:
- במחזור 5, כל 5 השלבים של הpipeline עובדים בו-זמנית - הוראה 1 ב-WB, הוראה 2 ב-MEM, הוראה 3 ב-EX, הוראה 4 ב-ID, הוראה 5 ב-IF.
- בלי pipeline, 5 הוראות היו לוקחות 5 x 5 = 25 מחזורים. עם pipeline, לקח רק 9 מחזורים.
תפוקה מול חביון - throughput vs latency¶
חשוב להבין נקודה מהותית: הpipeline לא מאיץ הוראה בודדת. הוראה אחת עדיין לוקחת 5 מחזורים מתחילתה ועד סופה (זה החביון - latency). מה שהpipeline משפר הוא את התפוקה - throughput - כמה הוראות מסתיימות ביחידת זמן.
כשהpipeline מלא ועובד בצורה חלקה, הוראה אחת מסתיימת בכל מחזור שעון. כלומר:
- חביון (latency): 5 מחזורים להוראה אחת (לא השתנה)
- תפוקה (throughput): הוראה אחת למחזור (שיפור פי 5!)
זה בדיוק כמו פס ייצור - מכונית אחת עדיין לוקחת שעה לייצר, אבל כל 12 דקות מכונית חדשה יורדת מהפס.
סכנות בצינור - pipeline hazards¶
במציאות הpipeline לא תמיד זורם בצורה חלקה. יש מצבים שבהם הוראה לא יכולה להתקדם לשלב הבא כי היא תלויה במשהו שעדיין לא מוכן. מצבים אלה נקראים סכנות - hazards, והם הבעיה המרכזית בתכנון pipeline יעיל.
סכנת נתונים - data hazard¶
סכנת נתונים קורית כשהוראה צריכה תוצאה של הוראה קודמת שעדיין לא סיימה. דוגמה:
add eax, ebx ; instruction 1: eax = eax + ebx
sub ecx, eax ; instruction 2: ecx = ecx - eax (needs the new eax!)
הוראה 2 צריכה לקרוא את eax, אבל הוראה 1 תכתוב את התוצאה ל-eax רק בשלב WB (מחזור 5). הוראה 2 מגיעה לשלב ID (קריאת אוגרים) כבר במחזור 3 - שני מחזורים מוקדם מדי!
Cycle: 1 2 3 4 5
add eax, ebx: [IF] [ID] [EX] [MEM] [WB] <-- writes eax here
sub ecx, eax: [IF] [ID] ... <-- needs eax here!
eax not ready yet
פתרון 1 - העברה - forwarding (bypassing):
במקום לחכות שהתוצאה תיכתב לאוגר, המעבד מעביר אותה ישירות מפלט שלב EX של הוראה 1 לכניסת שלב EX של הוראה 2. זהו קיצור דרך בחומרה - חוטים שמחברים את הפלט של יחידת הALU ישירות לכניסה שלה.
Cycle: 1 2 3 4 5 6
add eax, ebx: [IF] [ID] [EX] [MEM] [WB]
|
forwarding| (the result is forwarded directly)
v
sub ecx, eax: [IF] [ID] [EX] [MEM] [WB]
בזכות forwarding, הוראה 2 מקבלת את הערך החדש של eax בזמן - בלי עיכוב.
פתרון 2 - השהייה - stalling (bubbles):
כשforwarding לא מספיק (למשל, כשהוראה 1 היא load מזכרון - התוצאה מוכנה רק אחרי שלב MEM), המעבד מכניס "בועות" - מחזורים ריקים שבהם הpipeline עוצר ומחכה:
Cycle: 1 2 3 4 5 6 7
ldr eax, [mem]: [IF] [ID] [EX] [MEM] [WB]
|
| (the result is only ready here)
v
sub ecx, eax: [IF] [ID] [--] [EX] [MEM] [WB]
bubble!
הבועה (bubble) היא מחזור מבוזבז - הpipeline עוצר לרגע. זו הסיבה שload מזכרון ואחריו שימוש מיידי בתוצאה יוצר עיכוב.
סכנת בקרה - control hazard¶
סכנת בקרה קורית כשיש הוראת הסתעפות (branch) - כמו jz, jne, jmp - שמשנה את זרימת התוכנית. הבעיה: בזמן שהוראת ההסתעפות עדיין נמצאת בשלב ID או EX (וטרם חושב התנאי), הpipeline כבר הספיק לטעון הוראות מהכתובת הבאה. אם ההסתעפות כן מתקיימת, ההוראות שנטענו הן שגויות!
cmp eax, 0
jz label ; if eax == 0, jump to label
add ebx, 1 ; loaded into the pipeline - but might not need to run!
mov ecx, 5 ; this is also loaded too early
label:
sub edx, 1
פתרון 1 - חיזוי הסתעפויות - branch prediction:
המעבד מנחש לאן ההסתעפות תלך, וממשיך לטעון הוראות מהכתובת המנוחשת. אם ניחש נכון - מעולה, אין עיכוב. על חיזוי הסתעפויות נרחיב בפרק 8.2.
פתרון 2 - שטיפת צינור - pipeline flush:
אם הניחוש היה שגוי, המעבד צריך לשטוף את כל ההוראות השגויות מהpipeline ולהתחיל מחדש מהכתובת הנכונה. כל העבודה שנעשתה על ההוראות השגויות הולכת לפח. בpipeline של 5 שלבים זה אומר אובדן של 2-3 מחזורים. בpipeline עמוק של 15-20 שלבים, זה אובדן של 15-20 מחזורים - מחיר כבד מאוד!
סכנת מבנה - structural hazard¶
סכנת מבנה קורית כששתי הוראות צריכות את אותו רכיב חומרה בו-זמנית. דוגמה קלאסית: הוראה אחת נמצאת בשלב IF (צריכה לקרוא הוראה מהזכרון) בזמן שהוראה אחרת בשלב MEM (צריכה לקרוא/לכתוב נתונים מהזכרון). אם יש רק יציאה אחת לזכרון, הן לא יכולות לעבוד בו-זמנית.
פתרון: שכפול חומרה
הפתרון הנפוץ הוא לשכפל את הרכיב הבעייתי. למשל:
- הcache הוראות וcache נתונים נפרדים (L1I ו-L1D) - כך שלב IF קורא מcache אחד ושלב MEM קורא מcache אחר, ללא התנגשות
- יציאות קריאה מרובות בregister file - מאפשרות לכמה הוראות לקרוא אוגרים בו-זמנית
- יחידות חישוב מרובות - כמה ALU-ים, כך שכמה הוראות יכולות להיות בשלב EX בו-זמנית (נושא שנרחיב בפרק 8.4 על ביצוע superscalar)
מעבדי x86 מודרניים - pipeline עמוק¶
הpipeline של 5 שלבים שראינו הוא מודל פשוט ומוקדם. מעבדי x86 מודרניים של Intel ו-AMD משתמשים בpipelines הרבה יותר עמוקים:
| מעבד | שנה (בערך) | עומק pipeline |
|---|---|---|
| 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 שלבים |
למה pipeline עמוק יותר? כי כל שלב עושה פחות עבודה, מה שמאפשר תדר שעון (clock frequency) גבוה יותר. ה-Pentium 4 עם 31 שלבים הגיע ל-3.8 GHz - תדר גבוה מאוד לתקופתו.
אבל יש מחיר: ככל שהpipeline עמוק יותר, המחיר של branch misprediction גדל (יותר הוראות לזרוק בshutdown), ויותר מקומות שבהם יכולים להיווצר hazards. ב-Pentium 4 Prescott עם 31 שלבים, branch misprediction עלתה 31 מחזורים! Intel למד את הלקח וחזר לpipelines קצרים יותר (14-19 שלבים) בארכיטקטורת Core.
CISC מול RISC¶
RISC - Reduced Instruction Set Computer¶
ארכיטקטורות כמו ARM ו-RISC-V הן ארכיטקטורות RISC:
- הוראות באורך קבוע (32 ביט בARM)
- מספר קטן של צורות הוראה פשוטות
- רק הוראות load/store ניגשות לזכרון - פעולות חישוב עובדות רק על אוגרים
- קל לפענח, קל לבנות pipeline יעיל
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.
לדוגמה, ההוראה:
מפורקת בתוך המעבד לכמה uops:
1. חישוב הכתובת: ebx + ecx*4 + 8
2. קריאה מהזכרון: load temp, [address]
3. חיבור: add eax, temp
ה-uops הפנימיים זורמים בpipeline הפנימי בדיוק כמו הוראות RISC - אורך קבוע, פשוטים, קלים לתזמן. כך מעבדי x86 נהנים משני העולמות:
- תאימות לאחור - אפשר להריץ את אותו קוד x86 מאז 1978
- ביצועים גבוהים - הליבה הפנימית היא RISC-like עם pipeline יעיל
זו הסיבה שכשנדבר על ביצועים של מעבדי x86 מודרניים, לעתים קרובות נדבר על uops ולא על הוראות x86 - כי ה-uops הם מה שבאמת זורם בpipeline.
סיכום¶
| מושג | הסבר |
|---|---|
| צינור הוראות - pipeline | חפיפה בין שלבי ביצוע של הוראות שונות |
| תפוקה - throughput | כמה הוראות מסתיימות ביחידת זמן (משתפרת) |
| חביון - latency | כמה זמן לוקח להוראה אחת (לא משתנה) |
| סכנת נתונים - data hazard | תלות בתוצאה של הוראה קודמת |
| העברה - forwarding | קיצור דרך בחומרה לתוצאות |
| סכנת בקרה - control hazard | הסתעפות שמשנה את זרימת ההוראות |
| שטיפת צינור - pipeline flush | זריקת הוראות שנטענו בטעות |
| סכנת מבנה - structural hazard | שני שימושים באותו רכיב חומרה |
| מיקרו-פעולות - micro-ops | הוראות RISC פנימיות שx86 מפורק אליהן |
בפרק הבא (8.2) נעמיק בחיזוי הסתעפויות - אחד המנגנונים הקריטיים ביותר בביצועי מעבדים מודרניים.