1 / 36

אופטימיזציה של תוכניות

אופטימיזציה של תוכניות. הנושאים במצגת זאת: אופטימיזציות לא תלויות-מכונה הזזת קוד החלפת אופרטורים 'יקרים' צירוף תתי ביטויים משותפים כוונון בעזרת profiling : זיהוי צווארי בקבוק בזמני ריצה. מבוסס על Bryant & O’hallaron / Computer Systems: a programmer’s perspective.

annona
Download Presentation

אופטימיזציה של תוכניות

An Image/Link below is provided (as is) to download presentation Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author. Content is provided to you AS IS for your information and personal use only. Download presentation by click this link. While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server. During download, if you can't get a presentation, the file might be deleted by the publisher.

E N D

Presentation Transcript


  1. אופטימיזציה של תוכניות • הנושאים במצגת זאת: • אופטימיזציות לא תלויות-מכונה • הזזת קוד • החלפת אופרטורים 'יקרים' • צירוף תתי ביטויים משותפים • כוונון בעזרת profiling: • זיהוי צווארי בקבוק בזמני ריצה מבוסס על Bryant & O’hallaron / Computer Systems: a programmer’s perspective

  2. סיבוכיות: תיאוריה אל מול מציאות • בדר"כ אנחנו מודדים סיבוכיות אסימפטוטית של אלגוריתמים. • בפועל: אנחנו עובדים על תוכנה שלמה, לא רק אלגוריתם מבודד. • ניתן לשפר בקבוע – גם זה חשוב! • תכנות יעיל יכול להביא לשיפור ביצועים של פי 10 ויותר. • אופטימיזציה במספר רמות: אלגוריתם, מבנה נתונים, פרוצדורות ולולאות. • לא ניתן לעשות זאת ללא הבנה של איך המחשב עובד • כיצד תוכנה מהודרת ומקושרת (linked) • איך למדוד ביצועי תוכנה ולזהות צווארי בקבוק • איך לשפר קוד בלי להרוס את המודולריות שלו והכלליות שלו.

  3. סוגי אופטימיזציות • שתי משפחות של אופטימיזציות: • אופטימיזציות לא תלויות מכונה (הרצאה זאת) • אופטימיזציות תלויות מכונה (שעור הבא) • האפקטיביות מאוד תלויה במבנה הספציפי של המעבד

  4. מהדרים הם מתרגמים מרובי אופטימיזציות • ממפים ביעילות תוכניות לשפת מכונה • הקצאת רגיסטרים • אופטימיזציות 'זולות' לחישוב, מקומיות ושמרניות. • בדר"כ לא יכולים להשפיע על סיבוכיות אסימפטוטית • עדיין באחריות המתכנת לבחור את האלגוריתם הטוב ביותר • סיבוכיות O(…) עדיין יותר חשובה משיפורים בפקטור קבוע. • אבל גם אלה חשובים... • מרכיבים רבים בקוד לא מאפשרים להם לעשות אופטימיזציות מרחיקות ראות. • חשש מ memory aliasing • חשש מ "תופעות לוואי" (side-effects) של פרוצדורות.

  5. מגבלות של מהדרים • נדרשים לעמוד במגבלות חמורות: • אסור להם לשנות את הסמנטיקה של התוכנית תחת כל תנאי. • דבר זה מונע מהם לבצע שינויים שהיו מתגלים כלא נכונים רק במקרים שלעולם לא קורים בפועל. • התנהגות של התוכנית שהיא מובנת מאליה למתכנת, יכולה להיות מוסתרת מפני המהדר בגלל סגנון תכנות • למשל: טווח ערכים במערך בפועל קטן יותר מאשר הטווח המוכרז.

  6. מגבלות של מהדרים • רוב הניתוח המבוצע על ידי המהדר נעשה בתוך הפרוצדורות ולא ביניהן. • ניתוח של כלל התוכנית הוא לא מעשי בגלל אילוצי זמן. • מבוסס על מידע סטטי • לא יכול לזהות קלט שיתקבל בזמן ריצה, וכו'. • חייב להיות שמרני!

  7. אופטימיזציות שאינן תלויות מכונה • הזזת קוד (code motion) • הקטן את מספר הפעמים שקוד מורץ • בתנאי שזה לא ישנה את התוצאה • במיוחד כשמדובר בהוצאת קוד מלולאות for (i = 0; i < n; i++) { int ni = n*i; for (j = 0; j < n; j++) a[ni + j] = b[j]; } for (i = 0; i < n; i++) for (j = 0; j < n; j++) a[n*i + j] = b[j];

  8. הזזת קוד על ידי המהדר • בדר"כ מהדרים עושים עבודה טובה בטיפול במערכים ולולאות פשוטות. • Code Generated by gcc for (i = 0; i < n; i++) { int ni = n*i; int *p = a+ni; for (j = 0; j < n; j++) *p++ = b[j]; } for (i = 0; i < n; i++) for (j = 0; j < n; j++) a[n*i + j] = b[j]; multiply R1,R2 # i*n move 8(R3),R4 # a leal (R4,R2,4),R5 # p = a+i*n (scaled by 4) # Inner Loop .L40: move 12(R3),R4 # b move (R4,R6,4),R2 # b+j (scaled by 4) move R2,(R5) # *p = b[j] add $4,R5 # p++ (scaled by 4) increment R6 # j++ compare R2 R6 jl .L40 # loop if j<n

  9. דוגמה נוספת ל Code Motion • Procedure to Convert String to Lower Case • את המקרה הזה, כפי שנראה, המהדר לא יצליח לפתור. • האחריות – על המתכנת... void lower(char *s) { int i; for (i = 0; i < strlen(s); i++) if (s[i] >= 'A' && s[i] <= 'Z') s[i] -= ('A' - 'a'); }

  10. Lower Case Conversion ביצועים של • זמן ריבועי באורך המחרוזת.

  11. נתרגם לתוכנית goto... void lower(char *s) { int i = 0; if (i >= strlen(s)) goto done; loop: if (s[i] >= 'A' && s[i] <= 'Z') s[i] -= ('A' - 'a'); i++; if (i < strlen(s)) goto loop; done: } • strlenמבוצע בכל איטרציה • strlenליניארי באורך המחרוזת: מחפש את '\0' • זמן ריבועי

  12. שיפור הביצועים void lower(char *s) { int i; int len = strlen(s); for (i = 0; i < len; i++) if (s[i] >= 'A' && s[i] <= 'Z') s[i] -= ('A' - 'a'); } • הוצא קריאה ל strlen מחוץ ללולאה • שהרי התוצאה אינה משתנה בין האיטרציות. • סוג של code motion

  13. זמני ריצה • זמן ריצה ריבועי לעומת ליניארי

  14. החלפת אופרטורים 'יקרים' • Shift, add instead of multiply or divide 16*x --> x << 4 • ההשפעה תלויות מכונה: בכמה עולה כפל וחילוק • בפנטיום II כפל של מספר שלם צורך רק 4 CPU cycles. חילוק – הרבה יותר. • זהה סדרה של הכפלות: int ni = 0; for (i = 0; i < n; i++) { for (j = 0; j < n; j++) a[ni + j] = b[j]; ni += n; } for (i = 0; i < n; i++) for (j = 0; j < n; j++) a[n*i + j] = b[j]; קודם השתמשנו בכפל (ni = n *i)

  15. שימוש ברגיסטרים • קריאה וכתיבה לרגיסטרים מהירה בהרבה מקריאה/כתיבה לזיכרון. • המהדר לא יכול לקבוע איזה משתנים כדאי לשמור ברגיסטרים. • נותן קדימות למשתנים זמניים • נותן קדימות למשתנים המוגדרים על ידי המשתמש בעזרת המילה register

  16. צרוף ביטויים משותפים • בדר"כ מהדרים לא מנצלים ידע אריתמטי. /* Sum neighbors of i,j */ up = val[(i-1)*n + j]; down = val[(i+1)*n + j]; left = val[i*n + j-1]; right = val[i*n + j+1]; sum = up + down + left + right; int inj = i*n + j; up = val[inj - n]; down = val[inj + n]; left = val[inj - 1]; right = val[inj + 1]; sum = up + down + left + right; 3 multiplications: i*n, (i–1)*n, (i+1)*n 1 multiplication: i*n leal -1(R1),R2 # i-1 multiply R4,R2 # (i-1)*n leal 1(R1),R3 # i+1 multiply R4,R3 # (i+1)*n multiply R4,R1 # i*n

  17. כלים חשובים: • מדידה: • חשב זמן ריצה הנצרך על ידי קטעי קוד מסויימים • לרב המעבדים יש מוני שעון • לא קל להוציא מהם מידע שימושי... • השימוש ב profiler: • מספר הקריאות לכל פונקציה, וכו' • visual studio 6.0: • Project/settings/link: enable profiling • - הדר • - Build profile • gcc –O2 –pg prog. –o prog • ./prog • Executes in normal fashion, but also generates file gmon.out • gprof prog • Generates profile information based on gmon.out

  18. דוגמה ל profiling • המטרה • חשב תדירות של מלים במסמך טקסט • הצג רשימה ממוינת מהמילה התדירה ביותר לנדירה ביותר • צעדים: • הפוך ל lowercase • הפעל פונקציית גִבּוּב (hash function). • נתחיל עם סכום ערכי ASCII של המחרוזת. • קרא מלים והכנס לטבלת גבוב. • פונקציה רקורסיבית שמכניסה מלים חדשות בסוף. • עדכן מונה עבור כל מילה • מיין תוצאות • נפעיל אלגוריתם בשם insertion-sort. (אלגוריתם זה יוצר רשימה חדשה, כל פעם מוסיף אליה איברים מהרשימה המקורית במקום הנכון).

  19. דוגמה ל profiling • הקלט: • Collected works of Shakespeare • 946,596 total words, 26,596 unique • מימוש ראשוני: 9.2 שניות • Shakespeare’s • most frequent words

  20. Profilingועכשיו: • הוסף ל executable פונקציות למדידת זמן • מחשב בקירוב זמן בתוך כל פונקציה. • שיטת החישוב • כל זמן מה (לערך 10 ms) שולח פסיקה • קובע איזו פונקציה כרגע מבוצעת • מעדכן את מונה הזמן ב 10ms • גם מעדכן מונה של מספר הקריאות לפונקציה

  21. תוצאות ה- Profiling % cumulative self self total time seconds seconds calls ms/call ms/call name 86.60 8.21 8.21 1 8210.00 8210.00 sort_words 5.80 8.76 0.55 946596 0.00 0.00 lower1 4.75 9.21 0.45 946596 0.00 0.00 find_ele_rec 1.27 9.33 0.12 946596 0.00 0.00 h_add • סטטיסטיקת קריאות • מספר קריאות וסה"כ זמן בכל פונקציה • הבעיה בביצועים: • המיון לוקח זמן רב מדי:אלג' לא יעיל. • קריאה בודדת לוקחת 87% מזמן הריצה.

  22. אופטימיזציות • צעד ראשון: החלף פרוצדורת מיון • נשתמש בפונקציית הספרייה qsort

  23. אופטימיזציות נוספות • Iter first: במקום פונקציה רקורסיבית – לולאה. מוסיף איבר חדש לראש הרשימה. • מאט את הקוד • Iter last: כנ"ל, אבל הוסף איבר חדש לסוף הרשימה. • מלים נפוצות יותר יהיו קרוב לתחילת הרשימה. • Big table: טבלת גיבוב גדולה יותר • Better hash: פונקצית גיבוב טובה יותר • Linear lower: הוצאת strlenמהלולאה

  24. מספר מלים על Profiling • יתרונות • עוזר לזהות צווארי בקבוק בביצועים • שימושי בעיקר כשמדובר במערכת מורכבת עם רכיבים רבים. • מגבלות • תלוי קלט • למשל, הטכניקה של linear lower לא הראה את כוחו בגלל שרב המלים הן קצרות. • ייתכן שלא נזהה חוסר יעילות כגון זמן ריצה ריבועי במקום ליניארי. • מנגנון חישוב הזמן לא מאוד מדויק • לא עובד במקרים של תוכניות הרצות פחות מ 3 שניות.

  25. מדדי זמן • זמן אבסולוטי • בדר"כ ננו-שניות: 10–9 sec • סדר גודל מציאותי • Clock Cycles • טווח טיפוסי • 100 MHz • 108 cycles per second • Clock period = 10ns • 2 GHz • 2 X 109 cycles per second • Clock period = 0.5ns

  26. Cycles Per Element (CPE) • דרך נוחה למדידת ביצועים במקרה של וקטור / רשימת נתונים • Length = n • T = CPE*n + Overhead vsum1, vsum2 שתי תוכניות עם הפרש ליניארי בביצועים. vsum1 Slope = 4.0 vsum2 Slope = 3.5

  27. length 0 1 2 length–1 data    דוגמה:חיבור איברי וקטור • Procedures vec_ptr new_vec(int len) • Create vector of specified length int get_vec_element(vec_ptr v, int index, int *dest) • Retrieve vector element, store at *dest • Return 0 if out of bounds, 1 if successful int *get_vec_start(vec_ptr v) • Return pointer to start of vector data • Similar to array implementations in Pascal, ML, Java • E.g., always do bounds checking

  28. המשך דוגמה void combine1(vec_ptr v, int *dest) { int i; *dest = 0; for (i = 0; i < vec_length(v); i++) { int val; get_vec_element(v, i, &val); *dest += val; } } • מטרה • חשב סכום איברי הוקטור • שמור תוצאה ב dest. • Pentium III Performance: Clock Cycles / Element • 42.06 (Compiled -g) 31.25 (Compiled -O2) • (-g: debug information, -O2 : אופטימיזציות ברמה 2)

  29. לולאות... void combine1-goto(vec_ptr v, int *dest) { int i = 0; int val; *dest = 0; if (i >= vec_length(v)) goto done; loop: get_vec_element(v, i, &val); *dest += val; i++; if (i < vec_length(v)) goto loop done: } • חוסר יעילות: • השגרה vec_length נקראת בכל איטרציה • למרות שהתוצאה זהה. 1 iteration

  30. הוצא את vec_lengthאל מחוץ ללולאה void combine2(vec_ptr v, int *dest) { int i; int length = vec_length(v); *dest = 0; for (i = 0; i < length; i++) { int val; get_vec_element(v, i, &val); *dest += val; } } • אופטימיזציה • הוצא את vec_length מהלולאה הפנימית. • הערך לא משתנה. • Code motion • CPE: from 31.25 down to20.66 (Compiled -O2) • vec_lengthדורש זמן קבוע, אבל עם תקורה משמעותית.

  31. חוסמי אופטימיזציות: פונקציות • מדוע, בעצם, לא יכול המהדר להוציא את vec_len או את strlenמחוץ ללולאה הפנימית ? • לפונקציות יכול להיות side effects • ייתכן שהפונקציה משנה את המצב הגלובלי • ייתכן שהיא מחזירה ערכים שונים עבור קריאה עם אותם ארגומנטים. • ייתכן של - strlen יש אינטראקציה עם lower. • מדוע בעצם המהדר לא מסתכל על הקוד של vec_len,strlen ? • אם ההידור הוא לא סטטי, המקשר(linker) עלול לקשר עם גירסה אחרת של אותה פונקציה. • אופטימיזציות המערבות מספר פרוצדורות אינה מעשית בדר"כ בגלל זמן ריצה. • המהדר מתייחס לפונקציות כ- 'קופסה שחורה'

  32. עוד אופטימיזציות... void combine3(vec_ptr v, int *dest) { int i; int length = vec_length(v); int *data = get_vec_start(v); *dest = 0; for (i = 0; i < length; i++) { *dest += data[i]; } • אופטימיזציות • הימנע/י מקריאה לפונקציה כדי להביא את האלמנט הבא • מצא מצביע לתחילת הרשימה לפני הלולאה • בתוך הלולאה התייחס למצביע. • (פחות מודולרי ומתאים ל abstract data types) • CPE: 20.66  6.00 • קריאות לפונקציות הן יקרות • הורדנו גם את זמן בדיקת האינדקס (בדיקה שהוא בגבולות המערך).

  33. הקטן גישות לזיכרון void combine4(vec_ptr v, int *dest) { int i; int length = vec_length(v); int *data = get_vec_start(v); int sum = 0; for (i = 0; i < length; i++) sum += data[i]; *dest = sum; } • עוד אופטימיזציה... • אין צורך לשמור את התוצאה ב dest עד הסיום • המשתנה הלוקאלי sum נשמר ברגיסטר • חוסך קריאה אחת וכתיבה אחת לזיכרון בכל איטרציה. • CPE: 6.00  2.00 (we started from 31.25) • גישה לזיכרון צורכת זמן רב.

  34. חוסמי אופטימיזציות: Memory Aliasing • מדוע המהדר לא הפך את combine3 ל combine4 בעצמו? • התשובה:חשש מ Aliasing (כִּנוּי) • שתי התייחסויות שונות לזיכרון מתייחסות לאותו מיקום. v: [2, 3, 6] • combine3(v, get_vec_start(v)+2) --> האיבר האחרון בווקטור עכשיו יאחסן את התוצאה. [2,3,0] [2,3,2] [2,3,5] [2,3,10] • combine4(v, get_vec_start(v)+2) --> התוצאה שונה. [2,3,6] [2,3,6] [2,3,6] [2,3,11] מסקנה: combine3 וcombine4 הן לא בהכרח זהות בנוכחות כינויים.

  35. חוסמי אופטימיזציות: Memory Aliasing נשים לב: • ב C, aliasing נפוץ. • בגלל שניתן לבצע address arithmetic (כלומר, חישוב מפורש של כתובת) • יש גישה ישירה למבנה המאחסן את הנתונים. • כדאי להשתמש במשתנים לוקאליים • בעיקר בתוך לולאות • כך המהדר יודע שהוא לא צריך לבדוק aliasing.

  36. סיכום: אופטימיזציות שאינן תלויות מכונה. • הזזת קוד (Code Motion) • מהדרים יודעים לעשות זאת במקרים מסוימים: מערכים פשוטים ולולאות פשוטות. • לא יודעים לעשות זאת בנוכחות של 'כינויים' וקריאות לפונקציות. • החלפת אופרטורים יקרים • Shift, Add, instead of Multiply or Divide • בדר"כ מהדרים מצליחים לעשות זאת לבד, אבל לא תמיד. • בכל זאת, תלוי מכונה • כשאפשר, שמור מידע ברגיסטרים במקום בזיכרון • למהדרים קשה לזהות הזדמנויות, בגלל חשש לשימוש בכינויים. • איחוד ביטויים משותפים. • למהדרים יש יכולת אלגבראית מוגבלת.

More Related