תכנות ביצועי ג'אווה, חלק 2: עלות הליהוק

עבור מאמר שני זה בסדרה שלנו על ביצועי ג'אווה, המיקוד עובר לליהוק - מה זה, מה זה עולה ואיך אנחנו יכולים (לפעמים) להימנע מכך. החודש, אנו מתחילים בסקירה מהירה של יסודות השיעורים, האובייקטים וההפניות, ואז עוקבים אחר מבט על כמה נתוני ביצועים הארדקור (בסרגל צד, כדי לא לפגוע בציוץ!) ובהנחיות בנושא סוגים של פעולות אשר ככל הנראה יעניקו לעיכול שלך ב- Java Virtual Machine (JVM). לבסוף, אנו מסיימים במבט מעמיק כיצד נוכל להימנע מאפקטים נפוצים של מבנה כיתות העלולים לגרום לליהוק.

תכנות ביצועים בג'אווה: קרא את כל הסדרה!

  • חלק 1. למד כיצד להפחית את תקורה של התוכנית ולשפר את הביצועים על ידי שליטה ביצירת אובייקטים ואיסוף אשפה
  • חלק 2. צמצם שגיאות תקורה וביצוע באמצעות קוד בטוח מסוג
  • חלק 3. ראה כיצד אלטרנטיבות אוספים נמדדות בביצועים, וברר כיצד להפיק את המיטב מכל סוג

סוגי אובייקטים והתייחסות ב- Java

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

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

אם הגדרת מחלקה אינה מציינת (באמצעות extendsהסעיף בכותרת הגדרת הכיתה) מחלקה אחרת כהורה או כמעמד-על, היא מרחיבה באופן מרומז את java.lang.Objectהכיתה. המשמעות היא שכל כיתה מתרחבת בסופו של דבר java.lang.Object, באופן ישיר או באמצעות רצף של רמה אחת או יותר של כיתות הורים.

אובייקטים עצמם הם תמיד מקרים של מחלקות, וסוג האובייקט הוא המחלקה שהוא מופע. בג'אווה, אנחנו אף פעם לא מתעסקים ישירות עם אובייקטים; אנו עובדים עם הפניות לאובייקטים. לדוגמא, השורה:

 java.awt.Component myComponent; 

אינו יוצר java.awt.Componentאובייקט; זה יוצר משתנה ייחוס מסוג java.lang.Component. למרות שלפניות יש סוגים בדיוק כמו לאובייקטים, אין התאמה מדויקת בין סוגי התייחסות לסוגי אובייקט - ערך הפניה עשוי להיות null, אובייקט מאותו סוג כמו ההפניה, או אובייקט מכל סוג משנה (כלומר, מחלקה יורדת מ) סוג ההפניה. במקרה הספציפי הזה, java.awt.Componentהוא מעמד מופשט, ולכן אנו יודעים שלעולם לא יכול להיות אובייקט מאותו הסוג כמו ההתייחסות שלנו, אך בהחלט יכולים להיות אובייקטים של מחלקות משנה מסוג זה.

פולימורפיזם וליהוק

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

עם זאת, השיטה שבוצעה בפועל על ידי שיחה נקבעת לא על פי סוג ההפניה עצמה, אלא על פי סוג האובייקט המוזכר. זהו העיקרון הבסיסי של פולימורפיזם - מחלקות משנה יכולות לעקוף את השיטות המוגדרות בכיתת ההורים על מנת ליישם התנהגות שונה. במקרה של המשתנה לדוגמא שלנו, אם האובייקט שמפנה אליו היה למעשה מופע של java.awt.Button, השינוי במצב הנובע setLabel("Push Me")משיחה יהיה שונה מזה שהתקבל אם האובייקט המוזכר היה מופע של java.awt.Label.

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

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

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

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

להזהיר את הרוחות

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

וקטור פרטי someNumbers; ... חלל ציבורי doSomething () {... int n = ... מספר שלם = (מספר שלם) someNumbers.elementAt (n); ...}

קוד זה מציג בעיות אפשריות מבחינת בהירות ותחזוקה. אם מישהו אחר מאשר המפתחים המקוריים היו לשנות את קוד בשלב מסוים, הוא עשוי לחשוב באופן סביר שהוא יוכל להוסיף java.lang.Doubleאת someNumbersהאוספים, שכן זהו תת מחלקה של java.lang.Number. הכל היה מסתדר בסדר אם הוא ינסה את זה, אבל בנקודה מסוימת כלשהי בביצוע הוא עשוי java.lang.ClassCastExceptionלהיזרק כאשר ניסיון השחקנים אל java.lang.Integerהוצא להורג על ערך מוסף שלו.

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

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

סוגיית הביצועים

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

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

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

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

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

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

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

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

שיעורי בסיס וליהוק

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

// מחלקה בסיסית פשוטה עם מחלקות משנה מחלקה מופשטת ציבורית BaseWidget {...} מחלקה ציבורית SubWidget מרחיבה את BaseWidget {... public void doSubWidgetSomething () {...}} ... // מחלקת בסיס עם מחלקות משנה, תוך שימוש בערכה הקודמת של כיתות מחלקה מופשטת ציבורית BaseGorph {// היישומון המשויך ל- BaseWidget myWidget הפרטי הזה של גורף; ... // הגדר את הווידג'ט המשויך לגורף זה (מותר רק לקבוצות משנה) הריק מוגן setWidget (יישומון BaseWidget) {myWidget = widget; } // קבל את הווידג'ט המשויך ל- BaseWidget getWidget הציבורי של גורף () {החזר את myWidget; } ... // להחזיר גורף עם קשר כלשהו לגורף הזה // זה תמיד יהיה מאותו סוג כפי שהוא נקרא, אבל אנחנו יכולים רק // להחזיר מופע של המופשט הציבורי שלנו בכיתה הבסיס BaseGorph otherGorph () {. ..}} // מחלקה תת-גורפית באמצעות מחלקה ציבורית תת-מחלקה של יישומון SubGorph מרחיבה את BaseGorph {// מחזירה גורף עם קשר כלשהו ל- BaseGorph otherGorph הציבורית של גורף () {...} ... פומבית בטל anyMethod () {... / / הגדר את הווידג'ט שאנו משתמשים בו יישומון SubWidget = ... setWidget (יישומון); ... // השתמש ביישומון שלנו ((SubWidget) getWidget ()). doSubWidgetSomething (); ... // השתמש ב- otherGorph SubGorph אחר שלנו = (SubGorph) otherGorph (); ...}}