הפוך את Java למהיר: אופטימיזציה!

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

  • אם הקוד שלך כבר עובד, מיטוב זה הוא דרך בטוחה להציג באגים חדשים, ואולי עדינים

  • אופטימיזציה נוטה להקשות על ההבנה והתחזוקה של הקוד

  • חלק מהטכניקות המוצגות כאן מגבירות את המהירות על ידי צמצום הרחבת הקוד

  • אופטימיזציה של קוד לפלטפורמה אחת עשויה להחמיר אותה בפלטפורמה אחרת

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

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

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

אז למה לייעל?

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

הפוך את Java למהיר!

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

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

90/10, 80/20, צריף, צריף, טיול!

ככלל, 90 אחוז מזמן החריג של התוכנית מושקע בביצוע 10 אחוזים מהקוד. (יש אנשים המשתמשים בכלל 80 אחוז / 20 אחוז, אך הניסיון שלי בכתיבה ובאופטימיזציה של משחקים מסחריים בכמה שפות במשך 15 השנים האחרונות הראה שהנוסחה של 90 אחוז / 10 אחוז אופיינית לתוכניות רעבות ביצועים מכיוון שמעט משימות נוטות מבוצע בתדירות רבה.) לאופטימיזציה של 90 האחוזים האחרים של התוכנית (שם הושקעו 10 אחוז מזמן הביצוע) אין השפעה ניכרת על הביצועים. אם היית מצליח לגרום לכך ש -90 אחוז מהקוד יבוצע במהירות כפולה, התוכנית תהיה מהירה רק ב -5 אחוזים. אז המשימה הראשונה באופטימיזציה של הקוד היא לזהות את 10 האחוזים (לעתים קרובות זה פחות מזה) של התוכנית שגוזלת את רוב זמן הביצוע. זה לאלא תמיד במקום שאתה מצפה שיהיה.

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

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

הפחתת כוח

הפחתת חוזק מתרחשת כאשר פעולה מוחלפת בפעולה שווה ערך המבוצעת מהר יותר. הדוגמה הנפוצה ביותר של צמצום כוח היא באמצעות המפעיל המעבר מספרים שלמים להכפיל ולחלק ידי כוח של 2. לדוגמה, x >> 2ניתן להשתמש בו במקום x / 4, ואת x << 1מחליף x * 2.

ביטול תת ביטוי נפוץ

חיסול ביטוי משנה נפוץ מסיר חישובים מיותרים. במקום לכתוב

double x = d * (lim / max) * sx; double y = d * (lim / max) * sy;

ביטוי המשנה הנפוץ מחושב פעם אחת ומשמש לשני החישובים:

double depth = d * (lim / max); double x = depth * sx; double y = depth * sy;

תנועת קוד

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

for (int i = 0; i < x.length; i++) x [i] * = Math.PI * Math.cos (y); 

הופך להיות

כפול פיקוסי = Math.PI * Math.cos (y); for (int i = 0; i < x.length; i++)x [i] * = פיקוסי;

לולאת לולאות

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

כפול פיקוסי = Math.PI * Math.cos (y); for (int i = 0; i < x.length; i += 2) {x [i] * = פיקוסי; x [i + 1] * = פיקוסי; }

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

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

הכנסת המהדר לעבודה

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

javac, JIT, ומהדרי קוד מקורי

רמת האופטימיזציה javacשמבצעת בעת קומפילציה של קוד בשלב זה היא מינימלית. ברירת המחדל היא לבצע את הפעולות הבאות:

  • קיפול מתמיד - המהדר פותר כל ביטוי קבוע כזה i = (10 *10)שמצטבר ל i = 100.

  • קיפול ענף (רוב הזמן) - gotoנמנעים מקודי בתים מיותרים .

  • חיסול מוגבל של קוד מת - לא מייצר קוד להצהרות כמו if(false) i = 1.

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

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

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

javacמציע אפשרות ביצועים אחת שתוכל להפעיל: הפעלת -Oהאפשרות לגרום למהדר להטמיע שיחות שיטה מסוימות:

javac -O MyClass

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

למרבה הצער, בגרסאות 1.0 של מהדר ה- javac יש באג שייצור קוד שאינו יכול לעבור את מאמת קוד הביצה כאשר -Oנעשה שימוש באופציה. זה תוקן ב- JDK 1.1. . לדוגמא, אם השיעורים הבאים נערכים יחד באמצעות -Oהאפשרות

מחלקה A {intic static int x = 10; בטל סטטי ציבורי בטל getX () {להחזיר x; }} מחלקה B {int y = A.getX (); }

השיחה אל A.getX () בכיתה ב 'תוטמע בכיתה B כאילו B נכתב כ:

מחלקה B {int y = Ax; }

עם זאת, הדבר יגרום ליצירת קובצי byt לגשת למשתנה Ax הפרטי שייווצר בקוד B. קוד זה יפעל בסדר, אך מכיוון שהוא מפר את מגבלות הגישה של Java, הוא יסומן על ידי המאמת IllegalAccessErrorבפעם הראשונה שהקוד יבוצע .

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

פרופילים

למרבה המזל, ה- JDK מגיע עם פרופיל מובנה שיעזור לזהות היכן מושקע זמן בתוכנית. זה יהיה לעקוב אחר הזמן שבילה בכל שגרה ולכתוב את המידע לקובץ java.prof. כדי להפעיל את הפרופילר, השתמש -profבאפשרות בעת הפעלת המתורגמן Java:

java -prof myClass

או לשימוש עם יישומון:

java -prof sun.applet.AppletViewer myApplet.html

יש כמה אזהרות לשימוש בפרופיל. פלט הפרופילר לא קל במיוחד לפענוח. כמו כן, ב- JDK 1.0.2 הוא מקצר את שמות השיטות ל -30 תווים, כך שייתכן שלא ניתן יהיה להבחין בין כמה שיטות. למרבה הצער, עם ה- Mac אין שום דרך להפעיל את הפרופילר, כך שמשתמשי ה- Mac אין להם מזל. נוסף על כל אלה, דף מסמכי Java של Sun (ראה משאבים) כבר אינו כולל את התיעוד -profלאופציה). עם זאת, אם הפלטפורמה שלך אכן תומכת -profבאופציה, ניתן להשתמש ב- HyperProf של ולדימיר בולאטוב או ב- ProfileViewer של גרג ווייט כדי לעזור בפירוש התוצאות (ראה משאבים).

אפשר גם "לפרופיל" קוד על ידי הכנסת תזמון מפורש לקוד:

long start = System.currentTimeMillis(); // do operation to be timed here long time = System.currentTimeMillis() - start;

System.currentTimeMillis()מחזיר זמן ב -1 / 1000 שניות. עם זאת, בחלק מהמערכות, כמו מחשב Windows, יש טיימר מערכת עם רזולוציה פחותה (הרבה פחות) מ -1 / 1000 שנייה. אפילו 1/1000 של שנייה אינו מספיק זמן לתזמן מדויק של פעולות רבות. במקרים אלה, או במערכות עם טיימרים ברזולוציה נמוכה, ייתכן שיהיה צורך לתזמן כמה זמן לוקח לחזור על הפעולה n פעמים ואז לחלק את הזמן הכולל ב- n כדי לקבל את הזמן בפועל. גם כאשר פרופיל זמין, טכניקה זו יכולה להיות שימושית לתזמון משימה או פעולה ספציפיים.

להלן מספר הערות סגירה בנושא פרופיל:

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

  • נסו לערוך כל בדיקת תזמון בתנאים זהים

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

יישומון המידוד

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