אופטימזציות מוקדמות מדי

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

בנוסף פתחתי ערוץ בטלגרם עבור הבלוג. בערוץ הזה אני כותב פוסטים קצרים יותר שגם מגיעים לטוויטר, לינקדאין ולפייסבוק. אם אתם מעדיפים לקרוא שם ולא להתסמך על האלגוריתמים של הרשתות כדי למצוא אותי אז הנה הלינק: https://t.me/thecodeline

ועכשיו לתוכן

Premature optimization is the root of all evil

Donald Knuth

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

הסרטון המלא

הדיון שלנו מתחיל במחשבה על Performance, ואיך זה תלוי בשני דברים אחרים:

  • מהירות (Velocity) – באיזה מהירות אנחנו יכולים להוסיף פיצ'רים חדשים
  • גמישות (Adaptability) – כמה מהר המערכת יכולה להשתנות כאשר יש לנו דרישות חדשות.
משולש האיזונים

השקעה רק ב-Velocity

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

השקעה רק ב-Velocity

הכנסה של Adaptability

כמו שכתבתי מקודם, Adaptability זה היכולת לשנות קוד כאשר דרישות חדשות מגיעות. זה כולל שימוש בקומפוננטות ובאינטרפייסים.
בשילוב נכון אנחנו יכולים להגדיל את ה-Velocity שלנו בשימוש עם Adaptability. אבל כיוון שאנחנו לא מסוגלים לנחש את העתיד, אנחנו לא יכולים לדעת איזה use cases אנחנו לא נפגוש ואנחנו לא צריכים להתכונן אליהם.
ולכן אם נעשה מערכת שהיא לגמרי גמישה, אנחנו נשקיע המון מאמץ להתכונן למקרים שלעולם לא יגיעו למציאות.
השקעה בגמישות יכולה לפגוע בביצועים, כיוון שזה מצריך מאיתנו להכניס אבסטרקציה למערכת, שבמקרים מסויימים תפגע בביצועים שלה. אבל זה מחיר שבדר"כ אנחנו מוכנים לשלם כדי להיות מסוגלים להגיב מהר לשינויים

השקעה רק ב-Adaptability

אז אנחנו מכוונים לאמצע?

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

  • במשחק שנמצא בשלביו האחרונים לפני פרסום כנראה ישקיעו את רוב המאמצים בביצועים של הקוד. אנחנו נהיה מוכנים לוותר על פיצ'רים אחרונים או על גמישות כדי שהקוד ירוץ טוב.
  • בשלבים יותר מוקדמים של המשחק, אנחנו כנראה נרצה להיות במצב שמכניס פיצ'רים מהר (התמקדות ב-Velocity) או באפשרות להגיב מהר

דוגמת פייסבוק

כאשר מארק צוקרברג כתב את פייסבוק, הוא כתב את זה ב-PHP. למי שאינו מכיר PHP היא שפה קצת מוזרה, והיא לא טובה כאשר אנחנו מכוונים לסקייל מאוד גבוה.
האם לצוקרברג היה עדיף לכתוב את פייסבוק ב-C?
התשובה לפי CodeAesthetic היא לא, כי אז הוא לא היה יכול לכתוב את המערכת מהר. כאשר הוא נתקל בבעיות ביצועים כבר היו לו מפתחים חזקים שיעזרו לו לעשות את האופטימיזציה.

בעיות ביצועים

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

  1. בעיות מאקרו – בעיות ברמת עיצוב המערכת
  2. בעיות מיקרו – תיקונים קטנים

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

דוגמאות

מי שעוד לא נתקל בזה, יש דיון ענק באינטרנט על ההבדל בין i++ לבין ++i. המצדדים ב-++i יטענו שזה מהיר יותר, כיוון שהדרך השניה דורשת העתקה של i לפני ביצוע החישוב.
אבל האם זה באמת ככה? ובכן CodeAesthetic הלך אשכרה לבדוק ואם אתם רוצים את הפרטים המלאים אני ממליץ בחום לראות את הסרטון, אבל המסקנה שלו היא שעם אופטימיזציות של הקומפיילר, ברוב המקרים אין הבדל. אבל על המחשב שלו כאשר יש הבדל זה הבדל של 3.4 נאנו שניות.
במשך השלוש שעות שהוא עבד על המדידה, יצא לו 51,029,403,600 פעמים, שזה בעצם 173 שניות.

האם אתם הייתם מסיימים דיון על performance בפחות מזה? כנראה שלא. ומכאן הטענה שדיונים על performance הם בזבוז זמן.

בואו נסתכל על דוגמה נוספת

Java
class CurrentUsers {
    int[] users = new int[10];

    public void logIn(int userId) {
        for (int user = 0; user < users.length; user++) {
            if (users[user] == 0) {
                users[user] = userId;
                return;
            }
        }

        throw new RuntimeException("Out of room");
    }

    public boolean loggedIn(int userId) {
        for (int user = 0; user < users.length; user++) {
            if (users[user] == userId) {
                return true;
            }
        }
        return false;
    }
}

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

Java
class CurrenUsers {
    HashSet<Integer> users = new HasSet<>();

    public void logIn(int userId) {
        users.add(userId);
    }

    public boolean loggedIn(int userId) {
        return users.contains(userId);
    }
}

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

כללי אצבע לאופטימיזציות

אז הנה ההמלצות של CodeAesthetic לאיך לטפל בבעיות ביצועים:

  1. למדוד
  2. לנסות תיקון
  3. למדוד שוב

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

אז איך מנסים ביעילות?

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

סיכום הכללי אצבע

  1. קודם כל שתהיה לכם בעיית ביצועים אמיתית.
  2. למדוד
  3. לשנות מבנה נתונים ולמדוד שוב
  4. לתקן דברים שעולים מהפרופיילר ולמדוד שוב
  5. לנתח את הקוד מבחינת הקצאות זיכרון, לתקן ולמדוד שוב

לסיכום

בפוסט הזה דיברנו על דיונים על performance של הקוד שלנו. למדנו קצת על הגישה של CodeAesthetic לנושא, ולמה לדעתו לנהל דיונים תאורטיים על performance זה לא יעיל במיוחד.
אני מסכים עם העיקרון הראשון שלו, שעדיף קוד קריא על פני קוד סופר יעיל. כיוון שלטווח ארוך אנחנו יכולים להוריד את מספר הטעויות שיעשו אחרינו אם הקוד שלנו נקי ומסודר.
אבל בהחלט הדיון הזה מעורר תהיות ומחשבות על כל מיני הערות ב-code review שרשמתי בעבר.
אני ממליץ בחום לראות את הסרטון המלא עם כל הדוגמאות שלו.

השאר תגובה

Scroll to Top