צלול עמוק לתוך מהדר Java JIT החדש - גראל

1. סקירה כללית

במדריך זה נבחן לעומק את מהדר Java Just-In-Time החדש (JIT) החדש, שנקרא Graal.

נראה מהו הפרויקט Graal ונתאר את אחד החלקים שלו, מהדר JIT דינמי בעל ביצועים גבוהים.

2. מה זה a JIT מַהְדֵר?

בואו נסביר תחילה מה עושה מהדר JIT.

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

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

3. מבט מפורט יותר על מהדר JIT

יישום JDK על ידי אורקל מבוסס על פרויקט קוד הפתוח OpenJDK. זה כולל את מכונה וירטואלית של HotSpot, זמין מאז Java גרסה 1.3. זה מכיל שני מהדרים JIT קונבנציונליים: מהדר הלקוח, הנקרא גם C1 ומהדר השרת, הנקרא opto או C2.

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

3.1. קומפילציה מדורגת

כיום, התקנת Java משתמשת בשני מהדרי JIT במהלך ביצוע התוכנית הרגילה.

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

זוהי אסטרטגיית ברירת המחדל בה משתמשת HotSpot, הנקראת אוסף מדורג.

3.2. מהדר השרתים

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

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

4. פרויקט GraalVM

פרויקט GraalVM הוא פרויקט מחקר שיצר אורקל. אנו יכולים להסתכל על Graal כמספר פרויקטים מחוברים: מהדר JIT חדש הבונה על HotSpot ומכונה וירטואלית חדשה. הוא מציע מערכת אקולוגית מקיפה התומכת במערך גדול של שפות (Java ושפות אחרות מבוססות JVM; JavaScript, Ruby, Python, R, C / C ++ ושפות אחרות מבוססות LLVM).

אנו כמובן נתמקד בג'אווה.

4.1. Graal - מהדר JIT שנכתב בג'אווה

גראל הוא מהדר JIT בעל ביצועים גבוהים. הוא מקבל את קוד הביצוע של JVM ומייצר את קוד המכונה.

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

מהדר הגביע נוצר עם יתרונות אלה בחשבון. היא משתמשת בממשק המהדר JVM החדש - JVMCI כדי לתקשר עם ה- VM. כדי לאפשר את השימוש במהדר JIT החדש, עלינו להגדיר את האפשרויות הבאות בעת הפעלת Java משורת הפקודה:

-XX: + UnlockExperimentalVMOptions -XX: + EnableJVMCI -XX: + UseJVMCIC compiler

מה זה אומר זה אנו יכולים להפעיל תוכנית פשוטה בשלוש דרכים שונות: עם המהדרים המדורגים הרגילים, עם גרסת JVMCI של Graal ב- Java 10 או עם GraalVM עצמו.

4.2. ממשק מהדר JVM

ה- JVMCI הוא חלק מ- OpenJDK מאז ה- JDK 9, כך שנוכל להשתמש בכל OpenJDK או Oracle JDK סטנדרטיים להפעלת Graal.

מה ש- JVMCI מאפשר לנו לעשות הוא להוציא את האוסף המדורג הרגיל ולחבר את המהדר החדש שלנו (כלומר Graal) ללא צורך בשינוי דבר ב- JVM.

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

ממשק JVMCICompiler {byte [] compileMethod (byte [] bytecode); }

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

בעיקרו של דבר, כאשר מתקשרים ל compileMethod() של ה JVMCIC קומפילר ממשק, נצטרך להעביר a CompilationRequest לְהִתְנַגֵד. לאחר מכן היא תחזיר את שיטת Java שאנו רוצים להרכיב, ובשיטה זו אנו נמצא את כל המידע הדרוש לנו.

4.3. גראל בפעולה

הגראל עצמו מבוצע על ידי ה- VM, כך שהוא יתפרש תחילה ויורכב על ידי JIT כשיהיה חם. בואו לבדוק דוגמה, אשר ניתן למצוא גם באתר הרשמי של GraalVM:

מחלקה ציבורית CountUppercase {סטטי סופי int ITERATIONS = Math.max (Integer.getInteger ("חזרות", 1), 1); ריק סטטי ציבורי ראשי (String [] args) {משפט מחרוזת = String.join ("", args); עבור (int iter = 0; iter <ITERATIONS; iter ++) {if (ITERATIONS! = 1) {System.out.println ("- iteration" + (iter + 1) + "-"); } סה"כ ארוך = 0, התחל = System.currentTimeMillis (), אחרון = התחלה; עבור (int i = 1; i <10_000_000; i ++) {total + = משפט. charts () .filter (Character :: isUpperCase) .count (); אם (i% 1_000_000 == 0) {long now = System.currentTimeMillis (); System.out.printf ("% d (% d ms)% n", i / 1_000_000, עכשיו - אחרון); אחרון = עכשיו; }} System.out.printf ("סה"כ:% d (% d ms)% n", סה"כ, System.currentTimeMillis () - התחלה); }}}

כעת נרכיב אותו ונפעיל אותו:

javac CountUppercase.java java -XX: + UnlockExperimentalVMOptions -XX: + EnableJVMCI -XX: + UseJVMCIC compiler

זה יביא לפלט הדומה לזה:

1 (1581 ms) 2 (480 ms) 3 (364 ms) 4 (231 ms) 5 (196 ms) 6 (121 ms) 7 (116 ms) 8 (116 ms) 9 (116 ms) סה"כ: 59999994 (3436 גברת)

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

אם אנו רוצים לראות את הנתונים הסטטיסטיים של אוספי הגראלים עלינו להוסיף את הדגל הבא בעת ביצוע התוכנית שלנו:

-Dgraal.PrintCompilation = נכון

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

4.4. השוואה עם מהדר ה- Top Tier

בואו נשווה כעת את התוצאות שלעיל עם הביצוע של אותה תוכנית שהורכבה עם מהדר השכבה העליונה במקום. לשם כך, עלינו לומר ל- VM לא להשתמש במהדר JVMCI:

java -XX: + UnlockExperimentalVMOptions -XX: + EnableJVMCI -XX: -UseJVMCIC Compiler 1 (510 ms) 2 (375 ms) 3 (365 ms) 4 (368 ms) 5 (348 ms) 6 (370 ms) 7 (353 ms) ) 8 (348 ms) 9 (369 ms) סה"כ: 59999994 (4004 ms)

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

4.5. מבנה הנתונים מאחורי האלמוגים

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

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

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

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

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

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

4.6. גרפים בפועל

אנו יכולים לבחון את גרפי הגראל האמיתיים באמצעות IdealGraphVisualiser. כדי להפעיל אותו, אנו משתמשים ב- mx igv פקודה. עלינו גם להגדיר את ה- JVM על ידי הגדרת ה- -דגראל. Dump דֶגֶל.

בואו לבדוק דוגמה פשוטה:

ממוצע int (int a, int b) {return (a + b) / 2; }

יש לזה זרימת נתונים פשוטה מאוד:

בגרף לעיל, אנו יכולים לראות ייצוג ברור של השיטה שלנו. הפרמטרים P (0) ו- P (1) זורמים לתוך פעולת ההוספה שנכנסת לפעולת החלוקה עם הקבוע C (2). לבסוף, התוצאה מוחזרת.

כעת נשנה את הדוגמה הקודמת כך שהיא תחול על מערך מספרים:

ממוצע int (ערכי int []) {int sum = 0; עבור (int n = 0; n <values.length; n ++) {sum + = ערכים [n]; } סכום החזר / ערכים.אורך; }

אנו יכולים לראות שהוספת לולאה הובילה אותנו לגרף המורכב הרבה יותר:

במה שנוכל להבחין הנה:

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

מבנה נתונים זה נקרא לעיתים ים של צמתים, או מרק של צמתים. עלינו להזכיר כי מהדר C2 משתמש במבנה נתונים דומה, כך שהוא אינו משהו חדש, שחדש אך ורק עבור Graal.

ראוי לציין כי גראל מייעל ומרכיב את התוכנית שלנו על ידי שינוי מבנה הנתונים הנ"ל. אנו יכולים לראות מדוע זו הייתה בחירה טובה באמת לכתוב את מהדר ה- Graal JIT ב- Java: גרף הוא לא יותר מקבוצת אובייקטים עם אזכורים המחברים אותם כקצוות. מבנה זה תואם לחלוטין את השפה המכוונת לעצמים, שבמקרה זה ג'אווה.

4.7. מצב מהדר לפני הזמן

חשוב גם להזכיר זאת אנו יכולים להשתמש גם במהדר Graal במצב המהדר A-Time of Time ב- Java 10. כפי שכבר אמרנו, מהדר הגראלים נכתב מאפס. הוא תואם לממשק נקי חדש, JVMCI, שמאפשר לנו לשלב אותו עם HotSpot. זה לא אומר שהמהדר מחויב אליו.

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

הסיבה העיקרית לכך שנשתמש ב- Graal באופן זה היא להאיץ את זמן ההפעלה עד שגישת ה- Compilation Tiered הרגילה ב- HotSpot תוכל להשתלט עליה.

5. מסקנה

במאמר זה בחנו את הפונקציות של מהדר Java JIT החדש כחלק מהפרויקט Graal.

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

לאחר מכן, דיברנו על מבנה הנתונים ש- Graal משתמש בו כדי לתפעל את התוכנית שלנו ולבסוף, על מצב המהדר של AOT כדרך נוספת להשתמש ב- Graal.

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