The root of all evil

איכות תוכנה נמדדת בכמה פרמטים:
נכונות (correctness): האם התוכנה עושה מה כל מה שהיא צריכה לעשות, ובצורה נכונה?
קלות תחזוקה (maintainability) : כמה קל למצוא ולטפל בבאגים? כמה קל להרחיב את המערכת?
ביצועים (performance) : האם התוכנה עובדת מהר מספיק?
המדדים האלו נוטים לבוא אחד על חשבון השני.
קוד נכון וקל לתחזוקה הוא בדרך כלל פחות יעיל.
קוד נכון ויעיל הוא בדרך כלל קשה לתחזוקה.
קוד יעיל וקל לתחזוקה.. אין דבר כזה :).

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

בשלב הזה רוב המפתחים פשוט ירוצו וישכתבו חלקים מהקוד כדי לגרום להם לרוץ יותר מהר, בדרך כלל תוך העלאת המורכבות של הקוד. הבעיה היא שגם אם אכן יש שיפור ביצועים – איך נחליט אם הוא מצדיק את השינויים שעשינו?
המשמעות של השינויים האלו, היא כאמור עליה במורכבות הקוד, מה שיקשה עלינו להוסיף תכונות עתידיות לקוד ויקשה עלינו לאתר ולתקן באגים.
אולי השיפור בפועל הוא 2% בלבד ולא מצדיק את המחיר הזה?
כדי להחליט צריך להשוות את הביצועים לפני ואחרי השינוי, ולהחליט אם השיפור מצדיק את השינוי בקוד.
הדרך לעשות את זה היא ליצור בדיקה מוגדרת היטב שניתן להריץ שוב ושוב ולקבל מדידות זמן כמעט זהות בין הרצות שונות, ואז לראות בכמה אנחנו מצליחים לשפר את זמן הריצה של הקוד.
כדאי שהבדיקות יריצו בעיקר קוד אמיתי של התוכנית, למפתחים יש נטיה ליצור בדיקות סינטטיות שמראות שיפורי ביצועיים אסטרונומיים בנקודות מסויימת, אבל בתמונה הכללי הרבה פעמים השיפורים הם הרבה פחות משמעותיים.
דוגמא פשוטה:
קוד אמיתי מהתוכנית
[code lang="java"]
// do stuff
doSomething();
// do more stuff
[/code]
בדיקה סינטטית:
[code lang="java"]
int start = time();
for (int i=0;i<1000000;i++) { doSomething(); } int elapsed = time() - start; print "average time is " + (elapsed / 1000000); [/code] צריך לשים שהבדיקה הסינטטית בודקת תזמון של פעולה אחת בזרימה של התוכנית, ויש עוד פעולות (do stuff וdo more stuff). ככה שגם אם נשפר את הביצועים של הבדיקה ב90% - אם הפעולות האחרות לוקחות בסך הכל שניה, וdoSomething לוקחת 10 מילישניות אז השיפור הוא זניח במקרה הזה. בפוסט הבא בסדרה הזו אני אתאר דוגמא מעשית של אופטימיזציה של שאילתות MySQL

Facebook Comments

8 תגובות בנושא “The root of all evil”

  1. וואו, מעולה!

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

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

    הערה: אני מבין למה הכתוונת ב- "int later = time() – now;", אבל נראה לי שזה לא כתוב בסדר הנכון, כנראה בכלל הפלטפורמה ולא בגלל שלא רשמת נכון. האם אני צודק?

    אז אני ממתין לפוסטים הבאים.

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

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

  4. First I believe that the best way to do optimization (or more to the point – to know what to focus on when you have to optimize memory or runtime) is to run a profiling tool (jpob, jprofiler….) on a "real application scenarios". Then when you had drilled it down to few "flows/functions" write a unitest and possibly run it in a loop (so that you don't have to run a profiler every time).
    Also the loop in your code is itself not optimized (and thus is adds to the "average" time more that it has to). To optimize your loop do it like – for(int i = 1000000 ; i > 0 ; i–) as you probably know checking that the number in the registry is not 0 is a much faster check the to check number i against any other number (10000000 in this case).

  5. מוטי, אני מסכים שעדיף להשתמש בפרופיילר אם יש פרופיילר זמין לסביבה שלך, דבר די נדיר אצלי.
    (בין אם זה פיתוח לסלולרי, ואז אף פרופיילר לא ירוץ על הטלפון, או פיתוח מרובה סביבות כמו WEB שכולל PHP, MySQL וJavaScript באותה אפליקציה)

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

  6. "
    as you probably know checking that the number in the registry is not 0 is a much faster check the to check number i against any other number (10000000 in this case).
    "

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

סגור לתגובות.