1 תורת הקומפילציה הרצאה 11 יצירת הקוד Code Generation Aho, Lam, Sethi and Ullman – Chapter 8
2 2 יצירת קוד syntax analysis semantic analysis code generation syntax tree intermediate code הגענו ל -back-end: מייצרים קוד עבור מכונה ספציפית. הדיון מתאים לקוד שעבר אופטימיזציה או קוד לפני אופטימיזציה. ההנחה היא שקוד הביניים לא מכיל שגיאות ( בשלב זה לא נהוג לחפש שגיאות ).
3 3 מטרות יצירת קוד בעל איכות טובה יעילות בזמן ריצה יעילות במקום הנדרש בזמן ריצה לאחרונה - יעילות בצריכת אנרגיה גודל קוד קטן ככל האפשר ניצול יעיל של משאבי המכונה ( למשל, ניצול טוב של הרגיסטרים, או ניצול פקודות מכונה מהירות ייחודיות ) יצירת קוד בזמן סביר מותר לקומפיילר לרוץ לאט כדי ליצור תכנית שרצה מהר. אבל בגבולות הטעם הטוב. יצירת קוד אופטימלי היא NP-hard, לכן משתמשים באלגוריתמים היוריסטיים.
4 4 הקלט והפלט של ה -code generator קלט : קוד בשפת הביניים + מידע מטבלת הסמלים ( וכל מידע אחר שנרצה להעביר מה -front-end). פלט : שפת המטרה absolute machine code: מוכן להרצה מיידית ( מכיל כתובות אבסולוטיות ). תוכניות com ב -DOS לא ממש קיים בארכיטקטורות מודרניות relocatable code: כתובות יחסיות, מאפשר קומפילציה נפרדת. דורש link-load לפני הביצוע. קבצי obj ( קבצי tpu, וכו ') assembly language: דורש ביצע assembly לפני הריצה. יצירת קוד יותר קצר, מאפשר שימוש במקרו, פקודות סימבוליות.
5 5 אתגרים ניהול זיכרון - היכן נמצא משתנה במהלך הביצוע ? הבנת פקודות מכונה : אוסף רחב מצריך החלטה נבונה יותר באיזו אופציה עדיף להשתמש. מה העלות בזיכרון של כל פקודה ? צריכת אנרגיה, יעילות וכו '... הקצאת רגיסטרים בחירת סדר החישוב תוך התחשבות ב - pipelining וב - caching אנו נדבר על : עלות פקודות יצירת קוד לקפיצה לרוטינה ( וחזרה ) בלוקים בסיסיים, Control Flow Graph, ואופטימיזציות בסיסיות. ייצור קוד תוך כדי מעקב על מיקום המשתנים בזיכרון וברגיסטרים. הקצאת רגיסטרים.
6 6 מכונת מטרה לדוגמא byte addressability, 4-byte words, n רגיסטרים – R 0,... R n-1 מבנה הפקודות OP source, destination צורת האופרנדים addressformmode MMabsolute RRregister c + content ( R )c ( R )indexed content ( R )* Rindirect register content ( c + content ( R ) )* c ( R )indirect indexed c (constant – not an address) # cliteral
7 אורך הקוד שנוצר כל פקודה תופסת בין 1-3 מילים. אנו נניח שבמילה אחת אפשר להכניס : Op-code, שני מספרים של רגיסטרים, צורת האופרנד ( מיעון עקיף, ישיר, אינדקס וכיו " ב ). אבל לכל כתובת בזיכרון או קבוע יש להוסיף מילה ( ראה עמודה ימנית ). Length costaddressformmode 1MMabsolute 0RRregister 1c + content ( R )c ( R )indexed 0content ( R )* Rindirect register 1content ( c + content ( R ) )* c ( R )indirect indexed 1c (constant – not an address) # cliteral Op-code + info More for op 1 ? More for op 2 ?
8 8 דוגמאות MOV R 0,123 העתק את הערך של רגיסטר 0 לכתובת 123 בזכרון. אורך פקודה = 2. ADD 4(R 0 ),123 הוסף את הערך שבכתובת הזכרון {4 ועוד ערך רגיסטר 0}, לכתובת 123. אורך פקודה = 3. SUB *4(R 0 ),R 1 החסר את הערך שבתא הזכרון, שכתובתו נמצאת בכתובת R 0 +4, מ -R 1. (R 1 := R 1 - *(4+R 0 )) אורך פקודה = 2. MOV #1,R 0 כתוב את הערך "1" ל -R 0. אורך פקודה = 2.
9 ניהול הזיכרון אזור סטטי בזיכרון שמכיל קוד אזור סטטי שמכיל משתנים סטטיים ( גלובליים ) ה -heap: אזור שמנוהל דינאמית עבור אובייקטים שהתוכנית מקצה. ה -stack שבו מנוהלים רשומות ההפעלה של הרוטינות.
10 ניהול רשומות ההפעלה לפרוצדורות נפריד בין scoping סטטי ודינאמי. כזכור עבור scope דינאמי מנהלים מחסנית עבור כל שם משתנה. מייצרים כניסה חדשה במחסנית בכל פעם שנכנסים לבלוק שבו מוגדר המשתנה, עושים pop בכל פעם שיוצאים מבלוק כזה ( לכל משתנה המוגדר בו ). היתרון : אין צורך במחסנית של רשומות הפעלה, ולכל פרוצדורה יש רשומת הפעלה יחידה. המידע בטבלת הסמלים מפנה למחסנית של כל משתנה. את כתובות החזרה שומרים במחסנית ומעדכנים בכל כניסה ויציאה מרוטינה. נעבור ל -scoping סטטי. לא נדבר על ניהול המחסניות, אלא על איך נראית קריאה לרוטינה, חזרה מרוטינה, ורשומת ההפעלה.
11 Static scoping כאן המשתנים יושבים בתוך רשומת ההפעלה. הכתובות הן יחסיות ל -stack pointer. ראשית, נתייחס לקפיצה לרוטינה ולצעדים המתבצעים בקפיצה ובחזרה. בדוגמא שלנו ה -SP יצביע על ההתחלה של רשומת ההפעלה האחרונה ב - stack, ולכן ה -offset- ים יהיו חיוביים. כשנקפוץ לרוטינה, נקדם את ה -SP בגודל הרשומה של הרוטינה הקוראת. רשומת הפעלה קודמת רשומת הפעלה נוכחית SP כתובות עולות
12 ניהול מחסנית רשומות הפעלה ב -scoping סטטי main MOV #stackstart, SP … code for the main procedure … HALT calling a procedure ADD #caller.recordsize, SP MOV #here + 16, *SP GOTO callee.code_area SUB #caller.recordsize, SP return from a procedure GOTO *0 ( SP ) רגיסטר מיוחד, מצביע לראש המחסנית. יצביע לראש רשומת ההפעלה הנוכחית. נניח לשם פשטות שכתובת החזרה נמצאת בתחילת רשומת ההפעלה.
13 ניהול מחסנית – דוגמא /* code for main */ action 1 call q action 2 halt /* code for p */ action 3 return /* code for q */ action 4 call p action 5 call q action 6 call q return
14 code for qcode for main ACTION 4 300:initialize the stackMOVE #600,SP100: ADD #qsize,SP320:ACTION 1 108: push return addressMOV #344,*SP328:call sequence beginsADD #main_size,SP128: call pGOTO :push return addressMOV #152,*SP136: SUB #qsize,SP344:call qGOTO : ACTION 5 352:restore SPSUB #ssize,SP152: ADD #qsize,SP372:ACTION 2 160: push return addressMOV #396,*SP380:HALT180: call qGOTO :... SUB #qsize,SP396:code for p ACTION 6 404:ACTION 3 200: ADD #qsize,SP424:returnGOTO *0(SP)220: push return addressMOV #448,*SP432:... call qGOTO : SUB #qsize,SP448: returnGOTO *0(SP)456:... stack starts here600: ניהול מחסנית – דוגמא
15 דוגמא – נניח ש - x := 0 היא שורה בתוכנית המקור משתנים סטטיים נניח ש - x אמור להמצא בהיסט ( (offset12 בקוד ביניים נראה static [12] := 0 בקוד המכונה, אם האזור הסטטי מתחיל בכתובת 100, נקבל : MOV #0, 112 – קוד מכונה הקצאה של משתנים מקומיים במחסנית נניח ש - x משתנה מקומי הממוקם ברשומת ההפעלה המוצבעת ע " י SP t 1 := 12 + SP * t 1 := 0 בפקודות מכונה – MOV #0, 12 ( SP ) קוד ביניים ( רביעות ) חישוב כתובות עבור משתנים ( ב -scoping סטטי )
16 מבנה התוכנית המיוצרת הבנת ההקשר של הפקודה יכולה להואיל לייצור הקוד ( בפרט הקצאת רגיסטרים ובחירת הפקודות שבהן נשתמש ). מושג מאד חשוב בהבנת קוד הוא בלוק בסיסי Basic block -. בלוק בסיסי הוא סדרת פקודות עם כניסה יחידה ( לפקודה הראשונה ) – אין קפיצה בקוד הקופצת לאמצע בלוק. יציאה יחידה ( מהפקודה האחרונה ) – הקוד יתבצע ללא שום קפיצות עד הגעתו לפקודה האחרונה בבלוק, שעשויה להיות קפיצה או קפיצה מותנית, או פקודה שמהווה יעד של קפיצה ממקור אחר, או סיום התוכנית. הרעיון הוא שכאשר נכנסים לבלוק, עושים זאת מהפקודה הראשונה ואז עוברים את הפקודות בבלוק אחת אחת בלי לצאת. נגדיר קשת בין שני בלוקים אם הפקודה האחרונה של בלוק המקור עשויה לקפוץ אל הפקודה הראשונה של בלוק היעד.
17 Basic Blocks Example t 1 := 4 * i t 2 := a [ t 1 ] if t 2 <= 20 goto B 3 t 3 := 4 * i t 4 := b [ t 3 ] goto B 4 t 5 := t 2 * t 4 t 6 := prod + t 5 prod := t 6 goto B 4 False t 7 := i + 1 i := t 2 Goto B 5 B1B1 B2B2 B3B3 B4B4 True
18 פירוק תוכנית לבלוקים בסיסיים Input: A sequence of three-address statements Output: A list of basic blocks with each three-address statement in exactly one block Method We first determine the set of leaders (the first statements of basic blocks). The first statement is a leader Any statement that is the target of a conditional or unconditional jump is a leader Any statement that immediately follows a goto or conditional jump statement is a leader For each leader, its basic block consists of the leader and all statements up to but not including the next leader or the end of the program
19 (Control) Flow Graph צמתי הגרף – הבלוקים הבסיסיים קשתות הגרף – קשת מקשרת שני בלוקים אם קיימת הפנית בקרה מהראשון לשני ( או פשוט המשך ישיר של זרימת הבקרה ) לולאה – קבוצה קשירה היטב של בלוקים בעלת כניסה יחידה לולאה פנימית – לולאה ללא תת - לולאות הערה – נהוג להשתמש בשמות הבלוקים הבסיסיים כמטרת הקפיצה ( ולא במספרים סידוריים של הרביעיות המתאימות ) על מנת להקל הזזות של הבלוקים בסיסיים B1B1 prod := 0 i := 1 B2B2 t 1 := 4 * i t 2 := a [ t 1 ] t 3 := 4 * i t 4 := b [ t 3 ] t 5 := t 2 * t 4 t 6 := prod + t 5 prod := t 6 t 7 := i + 1 i := t 7 if i <= 20 goto B 2
20 דוגמא מהספר לקוד, וגרף זרימת בקרה מתאים
21 אופטימיזציות אפשר לקבל שיפורים משמעותיים בזמן בקוד גם כשמתבוננים רק בתוך בלוקים בסיסיים. כלומר : מנתחים את המידע הנמצא בתוך הבלוק ולא מתחשבים במה שקורה מחוץ לבלוק עושים שינויים לוקליים בלבד רמת דיוק גבוהה יותר משתמשת בכל הבלוקים שבתוך פרוצדורה. אם יש פיצול בזרימת הבקרה, לא יודעים מה קורה בחישוב בפועל, וצריך לעבוד באופן שמרני או ליברלי, כתלות במטרת האופטימיזציה. שיטות אלו יותר מסובכות ונדבר עליהן בהמשך הקורס. ניתן לדייק עוד יותר אם מתחשבים בכל הבלוקים שבתוכנית אבל : לא תמיד כל הבלוקים ידועים ( קישור לספריות לאחר הקימפול, וכיו " ב ) 21
22 הגדרה, שימוש, וחַיוּת של משתנים הקצאת משתנים נכונה לרגיסטרים היא קריטית ליעילות הריצה. הידיעה מתי ערכים בתוכנית רלוונטיים או " חיים " היא קריטית להקצאה יעילה של רגיסטרים. משפט מהסוג x := y + z מגדיר את x ומשתמש ב - y ו - z משתנה הוא חי בנקודה נתונה אם נעשה שימוש בערך שלו לאחר הנקודה האמורה. אם משתנה x מוגדר בפקודה i, ופקודה j משתמשת ב -x, ויש מסלול חישוב מ -i ל -j ( שאינו משנה את x), אז נאמר שפקודה j משתמשת בערך של x שהוגדר בפקודה i. בפרט, x חי בפקודה i ובכל המסלול בין i ל -j. נחשב שימוש של משתנים במקרה הפשוט שהכול קורה בתוך בלוק בסיסי. ( עבודה עם כמה בלוקים תשתמש ב -Data Flow Analysis: בשיעור הבא ).
23 טבלת ה -next-use עבור כל שורה בבלוק נתון, נרצה לשמור ( עבור כל משתנה אליו מתייחסת שורה זו ): האם המשתנה חי ( כלומר יש בו שימוש בעתיד ), ואם הוא חי באיזו פקודה יקרה השימוש הבא במשתנה. החישוב נעשה על - ידי סריקה של הבלוק מסופו לעבר תחילתו הנחות שמרניות : כל המשתנים שאינם מקומיים בבלוק, חיים בסוף הבלוק ( כלומר, נעשה בהם שימוש בהמשך ). שימו לב שיש הרבה משתנים זמניים לוקליים ב - intermediate code, ולכן אפילו טיפול לוקלי הוא רלוונטי. נניח שהמידע מי לוקלי לבלוק מושג באיזושהי צורה יעילה. ( קל לבדוק את זה במעבר על התוכנית.)
24 חישוב ה -next use חישוב ה - next-use – ברמת הבלוק הבודד – מהסוף להתחלה. נעזר בטבלת הסמלים לשמור מידע על המשתנים. נניח שבטיפול מהסוף להתחלה הגענו לפקודה i: i: x := y op z 1. נצמיד ל - i את האינפורמציה שנאספה בטבלת הסמלים על y, x ו - z עד עתה. 2. נקבע בטבלת הסמלים ( עבור הפקודות המוקדמות יותר בבלוק ): x – " לא חי "; " אין שימוש בהמשך " 3. נקבע בטבלת הסמלים : y, z – " חיים "; " השימוש הבא – ב - i" הערה – הסדר בין 2 ו - 3 – חשוב כי x עשוי להיות y x := y := x z := x * x := y * z
25 טרנספורמציות פשוטות ( וחשובות ) בתוך בלוקים בסיסיים סילוק ביטויים משותפים ביטול " קוד מת " השמה למשתנה שלא חי לאחר מכן החלפת סדר בין פקודות שאינן תלויות אחת בשנייה לקיצור הזמן בו משתנה חי ( כלומר הזמן שהוא דורש רגיסטר ) לייעול pipeline או מקבול פנימי. := b + ca := a – db := b + cc := a – dd d := b
26 טרנספורמציות פשוטות ( וחשובות ) בתוך בלוקים בסיסיים פישוטים אלגבריים : שוויונות : x + 0 = 0 + x = x – 0 = x x * 1 = 1 * x = x / 1 = x שימוש בפקודות זולות יותר : x ^ 2 = x * x 2 i * x = shift-left(x,i) 2 * x = x + x x / 2 i = shift-right(x,i) וכולי...
27 תהליך פשוט ליצירת קוד נייצר את הקוד פקודה פקודה, נטפל בכל בלוק בנפרד. לכל פקודה נבדוק היכן נמצאים האופרנדים נניח, למען הפשטות מחשב RISC, שבו הפקודות עובדות על רגיסטרים, פרט ל -store ו -load.
28 מעקב אחרי משתנים ורגיסטרים (Descriptors) נתעלם כרגע מרגיסטרים שאנו עושים בהם שימוש גלובלי כמו stack- pointer וכיו " ב ונניח שישנם רגיסטרים לשימושינו בבלוק. לכל רגיסטר – נרשום ב -descriptor מה יש בו בתחילת כל בלוק כל הרגיסטרים ריקים במהלך ייצור הקוד, כל רגיסטר יכיל משתנה אחד או יותר פקודה העתקה מהסוג x := y תגרום לרגיסטר להחזיק ערך של יותר ממשתנה אחד. לכל משתנה של התוכנית יש address descriptor המחזיק את הכתובת ( או הכתובות ) בו הוא נמצא. רגיסטר, כתובת בזיכרון, מיקום ב -stack. בד " כ ה -descriptor נשמר בטבלת הסמלים.
29 אלגוריתם ל -code generation קלט : רצף של פקודות three-address מבלוק בסיסי מסוים. For each three-address statement x := y op z, perform the following: 1. Invoke getreg (x := y op z) to determine locations R x, R y, and R z. 2. If Ry does not contain y, issue: “LD R y, y’ ”, for a location y’ of y. 3. If Rz does not contain z, issue: “LD R z, z’ ”, for a location z’ of z. 4. Issue the instruction “OP R x,R y,R z ” 5. Update the address descriptors of x, y, z, if necessary. In particular, R x is the only location of x now, and R x contains only x (remove R x from other address descriptors).
30 יצירת קוד – פעולות אחרות הטיפול בפעולות אונריות – דומה x := y מצריך עדכון המתארים עם סיום העבודה ברמת הבלוק הבסיסי שומרים את ערכי המשתנים שלא נמצאים במקומם הטבעי ( למשל נמצאים רק בתוך רגיסטרים ). אלא אם יש לנו אינפורמציה מקיפה יותר של live-dead בין בלוקים.
31 הקצאת רגיסטרים ( הפונקציה getreg) קלט : x := y op z המטרה – הקצאת רגיסטר ל - x, y, ו -z. נתחיל ב -y, בחירת עבור z שקולה. 1. אם y ברגיסטר R, קבע R y =R. 2. אחרת, אם יש רגיסטר פנוי R, קבע R y =R. 3. אחרת, אם יש רגיסטר המכיל רק את x, וגם x אינו אופרנד (z במקרה זה ) אז קבע R y =R. 4. אחרת, אם יש רגיסטר R שהמשתנה שבו v אינו חי, קבע R y =R. 5. אחרת אם יש רגיסטר R שהמשתנה שבו v קיים גם במקום אחר, קבע R y =R. 6. אחרת, בחר רגיסטר R כלשהו שמייצג מספר מינימלי של משתנים {v1, v2, …, vm}. לכל משתנה שמיוצג רק ע " י R העתק את ערכו לזיכרון. קבע R y =R. 7. בכל מקרה עדכן את ה -descriptor של R, v, ( או v i אם רלוונטי ) בהתאם.
32 חלק שני : הקצאת רגיסטר ל -x קלט : x := y op z 1. אם x ברגיסטר R המכיל רק את x, קבע R x =R. ( זה בסדר גם אם y או z זהים ל -x). 2. אחרת, אם לא משתמשים ב -y ( או ב -z) לאחר הפקודה הנוכחית, והרגיסטר שנקבע לו מייצג אותו באופן יחיד, אז ניתן לקבוע את הרגיסטר של x להיות זהה לרגיסטר של y ( או z). ההמשך מאד דומה 3. אחרת, אם יש רגיסטר פנוי R אחרת, אם יש רגיסטר R שהמשתנה שבו v אינו חי אחרת, בחר רגיסטר R כלשהו שמייצג מספר מינימאלי של משתנים {v1, v2, …, vm}. לכל משתנה העתק את ערכו לזיכרון, וקבע R x =R. 6. בכל מקרה עדכן את ה -descriptor של R, v, ( או v i אם רלוונטי ) בהתאם.
33 הקצאת רגיסטרים : דוגמא address descriptorregister descriptorcode generatedstatements registers empty a in R1, a in a, b in b, c in c, d in d, t in R2 R1 contains a R2 contains t LD R1, a LD R2, b SUB R2, R1, R2 t := a – b a in a, b in b, c in c, c in R3, d in d, t in R2, u in R1 R1 contains u R2 contains t R3 contains c LD R3, c SUB R1, R1, R3 u := a – c a in a, b in b, c in c, c in R3, d in d, t in R2, u in R1, v in R3 R1 contains u R2 contains t R3 contains v ADD R3, R2, R1v := t + u a in a, b in b, c in c, d in d, c in R3, d in d, t in R2, d in R1, v in R3 R1 contains d R2 contains t R3 contains v ADD R1, R1, R3 ST d, R1 d := v + u d := (a – b) + (a – c) + (a – c)
34 Register Allocation and Assignment Register allocation: איזה ערכים יישמרו ברגיסטרים. Register assignment: איזה ערך יישמר באיזה רגיסטר. ההחלטות על מיקום ערכים ברגיסטרים הן מהחשובות באופטימיזציה של קוד. הסיבה : פקודות הניגשות לרגיסטרים הן קצרות יותר ומהירות יותר. ההחלטה על register allocation היא NP- קשה ( אפילו אם יש רק רגיסטר יחיד במכונה ). לכן משתמשים בהיוריסטיקות. לעיתים ההיוריסטיקות מסתבכות עוד יותר בשל אילוצי חומרה. למשל מכונות מסויימות משתמשות ב pairing-. Mul R2,R3 מכניס את המכפלה של המספר ב -R2 והמספר ב -R3 אל הזוג R2,R3 כאשר R2 ו -R3 מוגדרים מראש כצמד. 34
35 Register Allocation and Assignment שתי גישות קיצוניות אפשריות : 1. הקצאה גלובלית : כל רגיסטר יכול להכיל כל משתנה. 2. לחלק את הרגיסטרים לקבוצות, כשלכל קבוצה אחראית על נושא מסוים. למשל, קבוצה אחת מחזיקה רק פוינטרים, אחרת מטפלת בחישובים אריתמטיים, שלישית משמשת רק ככתובות בסיס ( למערכים, ל - stack, וכו '.), עוד רגיסטר ישמש להחזרת פרמטר מרוטינות וכיו " ב. הגישה השלטת היא גישת ביניים : יש רגיסטרים ספציפיים שיוקצו ל -stack pointer, לשימוש כבסיס וכו ', אבל הרוב מוקצים באופן גלובלי. 35
36 הקצאת רגיסטרים גלובלית עד עתה דיברנו רק על הקצאה בתוך בלוק בסיסי, והנחנו שערכם של משתנים נכתב חזרה לזיכרון בסוף כל בלוק. על - מנת לחסוך חלק מה -loads וה -stores נרצה לעיתים לבחור משתנים שיש אליהם גישה תדירה ולהקצות להם רגיסטר שנשמר במעבר בין בלוקים. ההעדפה הראשונה ניתנת למשתנים " הכי פעילים " המופיעים בתוך הלולאות הפנימיות ביותר : אלו קטעי הקוד שמתבצעים ( בד " כ ) הכי הרבה. נקצה רגיסטרים עבור משתנים אלה ; השאר ישמשו להקצאה לוקלית בבלוק. הערכת התועלת של שמירת משתנה v מבלוק B ברגיסטר נעשית ע " י ספירה : לכל מופע של v ב -B שבו ניתן להשתמש ברגיסטר נוסיף נקודה. לכל מופע של v בבלוק עוקב שמשתמש בערך ( בלי להציב ב -v ערך חדש ) נוסיף שתי נקודות על כך שלא בצענו store ו -load במעבר בין הבלוקים. לאחר שנעריך תועלת לכל משתנה בלולאה הפנימית, נקצה רגיסטרים למשתנים שעבורם התועלת גבוהה ביותר.
37 Spilling ו -Graph Coloring כאשר צריך להקצות רגיסטר וכל הרגיסטרים תפוסים, צריך לבחור רגיסטר לשימוש, ולשפוך (spill) את ערכו הקודם לשמירה בזיכרון. המצב האידיאלי הוא שמקצים רגיסטר חדש כאשר ערך ברגיסטר קודם " מת " וניתן להשתמש ברגיסטר הקודם ללא spilling. מציאת הקצאת רגיסטרים שמביאה למינימום spilling היא NP- קשה. לכן משתמשים בהיוריסטיקות הקשורות לצביעת גרפים. נסתכל על תמונת הערכים החיים בתוכנית : חי v 1 חי v 2 חי v 3 חי v 4 חי v 5 חי v 6 חי v 7 חי v 8 זמן ריצה
38 בניית גרף מתאים נקצה צומת לכל משתנה, וקשת בין שני משתנים " מתנגשים ". צמתים מתנגשים אם יש חפיפה בזמן חייהם. משמעות : אם נקצה להם אותו רגיסטר אז יידרש spill. זמן ריצה חי v 1 חי v 2 חי v 3 חי v 4 חי v 5 חי v 6 חי v 7 חי v 8 V1V1 V1V1 V8V8 V8V8 V2V2 V2V2 V4V4 V4V4 V7V7 V7V7 V6V6 V6V6 V5V5 V5V5 V3V3 V3V3
39 בניית גרף מתאים נקצה צומת לכל משתנה, וקשת בין שני משתנים " מתנגשים ". צמתים מתנגשים אם יש חפיפה בזמן חייהם. משמעות : אם נקצה להם אותו רגיסטר אז יידרש spill. V1V1 V1V1 V8V8 V8V8 V2V2 V2V2 V4V4 V4V4 V7V7 V7V7 V6V6 V6V6 V5V5 V5V5 V3V3 V3V3 מטרתנו : להקצות k רגיסטרים למשתנים כך ששני משתנים יוקצו לרגיסטרים שונים אם יש ביניהם קשת. בעיית הצביעה : להקצות k צבעים לצמתים כך שני צמתים שכנים יקבלו צבע שונה. בעיית ה -k צביעה NP- קשה עבור k>2. אבל כאן מדובר בגרפים מאד ספציפיים : גרפי אינטרוואלים, שעבורם מציאת צביעה ב -k ניתנת לפיתרון יעיל. מצד שני, התוכנית האמיתית אינה גרף אינטרוואלים פשוט, בשל ה -control flow האמיתי.
40 היוריסטיקה 1. נמצא צביעה לגרף באופן הבא. 2. באופן איטרטיבי נעיף מהגרף כל צומת שדרגתה קטנה מ -k ( עם כל קשתותיה ). 3. הסבר : לא משנה אילו צבעים נבחר לצמתים האחרים, תמיד נוכל לצבוע צומת זאת בצבע חוקי. V1V1 V1V1 V8V8 V8V8 V2V2 V2V2 V4V4 V4V4 V7V7 V7V7 V6V6 V6V6 V5V5 V5V5 V3V3 V3V3 4. נשארנו עם צמתים שכולם מדרגה גדולה או שווה ל -k, או עם גרף ריק. 5. אם הגרף ריק, נחזיר את הצמתים שהעפנו אחת אחת ונצבע אותן. אחרת, 6. נבחר צומת כלשהו ונעיף אותו מהגרף. המשמעות : משתנה זה לא יקבל רגיסטר אלא יוקצה מהזיכרון. נחזור על שלב זה עד שייוצר צומת עם דרגה קטנה מ -k ואז נתחיל מהתחלה.
41 היוריסטיקה 1. נמצא צביעה לגרף באופן הבא. 2. באופן איטרטיבי נעיף מהגרף כל צומת שדרגתה קטנה מ -k ( עם כל קשתותיה ). 3. הסבר : לא משנה אילו צבעים נבחר לצמתים האחרים, תמיד נוכל לצבוע צומת זאת בצבע חוקי. V1V1 V1V1 V8V8 V8V8 V2V2 V2V2 V4V4 V4V4 V7V7 V7V7 V6V6 V6V6 V5V5 V5V5 V3V3 V3V3 4. נשארנו עם צמתים שכולם מדרגה גדולה או שווה ל -k, או עם גרף ריק. 5. אם הגרף ריק, נחזיר את הצמתים שהעפנו אחת אחת ונצבע אותן. אחרת, 6. נבחר צומת כלשהו ונעיף אותו מהגרף. המשמעות : משתנה זה לא יקבל רגיסטר אלא יוקצה מהזיכרון. נחזור על שלב זה עד שייוצר צומת עם דרגה קטנה מ -k ואז נתחיל מהתחלה. הפעולה ההיוריסטית העיקרית כאן היא בחירת הצומת להעיף בעת ביצוע שלב 6, והיא קובעת כמה spills באמת יהיו.
42 שימו לב שלא השגנו פיתרון אופטימאלי ראשית, יתכן שיש פיתרון אחר שבו מוותרים בהיוריסטיקה על צמתים אחרים ומתקבלים פחות spills. שנית, יצרנו את הגרף מתוך הנחות שמרניות של חפיפה בזמן החיים של משתנים. תיתכנה תלויות של גרף הזרימה כך ששני משתנים לעולם לא באמת חיים יחד, אבל אנחנו לא מודעים לכך בניתוח השמרני הפשוט. אבל – אם מצאנו צביעה מלאה של הגרף, כלומר, לא דרושים spills וכל המשתנים יכולים לחיות ברגיסטרים, אז הפיתרון שמצאנו הוא ( כמובן ) אופטימאלי.
43 לסיכום : יצירת קוד מעבר משפת ביניים לשפת מטרה (back-end) מכונת מטרה לדוגמא. אורך הקוד ויעילותו תלוי בניצול יעיל של הרגיסטרים. ייצור קוד לניהול רשומות הפעלה בלוקים בסיסיים, control flow graph, חַיוּת של משנים. טרנספורמציות פשוטות בתוך בלוקים בסיסיים. ייצור קוד תוך כדי מעקב על מיקום המשתנים בזיכרון וברגיסטרים. הקצאת רגיסטרים.