مرحبا يا هبر! أقدم لكم ترجمة المقال " فهم كيفية عمل Graal - مترجم جافا JIT مكتوب بلغة جافا ".
مقدمة
أحد الأسباب التي جعلتني باحثًا في لغات البرمجة هو أنه في مجتمع كبير من الأشخاص المشاركين في تكنولوجيا الكمبيوتر ، يستخدم الجميع تقريبًا لغات البرمجة ، والعديد منهم مهتمون بكيفية عملهم. عندما تعرفت على البرمجة لأول مرة عندما كنت طفلاً وأصبحت على دراية بلغة برمجة ، كان أول شيء أردت معرفته هو كيف تعمل ، وأول ما أردت فعله هو إنشاء لغتي الخاصة.
في هذا العرض التقديمي ، سأوضح لك بعض آليات العمل للغة التي تستخدمها جميعًا - Java. تكمن الخصوصية في أنني سأستخدم مشروعًا يسمى Graal ، والذي يطبق مفهوم Java في Java .
Graal هو واحد فقط من المكونات في عمل Java - فهو مترجم في الوقت المناسب . هذا هو جزء من JVM الذي يحول Java bytecode إلى كود الآلة أثناء تنفيذ البرنامج ، وهو أحد العوامل التي تضمن أداء عالي للمنصة. كما يبدو لي أيضًا ، ما يعتبره معظم الناس أحد أكثر الأجزاء تعقيدًا وغموضًا في JVM ، وهو ما يتجاوز إدراكهم. تغيير هذا الرأي هو الغرض من هذا الخطاب.
إذا كنت تعرف ما هو JVM ؛ بشكل عام فهم ما تعنيه المصطلحين من قبل رمز البايت ورمز الآلة ؛ وقادرة على قراءة التعليمات البرمجية المكتوبة بلغة جافا ، وآمل أن يكون هذا كافياً لفهم المواد المقدمة.
سأبدأ بمناقشة لماذا قد نريد مترجمًا JIT جديدًا لـ JVM مكتوبًا في Java ، وبعد ذلك سأوضح أنه لا يوجد شيء إضافي خاص في هذا ، كما قد تعتقد ، عن طريق تقسيم المهمة إلى تجميع مترجم واستخدام وتوضيح ذلك أن رمزها هو نفسه كما في أي تطبيق آخر.
سوف أتطرق إلى النظرية قليلاً ، ثم سأوضح كيف يتم تطبيقها أثناء عملية التجميع بأكملها من البايت إلى كود الآلة. سأعرض أيضًا بعض التفاصيل ، وفي النهاية سنتحدث عن مزايا هذه الميزة بالإضافة إلى تنفيذ Java في Java من أجلها.
سأستخدم لقطات شاشة الكود في Eclipse ، بدلاً من تشغيلها أثناء العرض التقديمي ، لتجنب المشاكل الحتمية للتشفير المباشر.
ما هو مترجم JIT؟
أنا متأكد من أن الكثير منكم يعرف ما هو مترجم JIT ، ولكن ما زلت أتطرق إلى الأساسيات حتى لا يجلس أحد هنا خائفًا من طرح هذا السؤال الرئيسي.
عند تشغيل الأمر javac
أو compile-on-save في IDE ، يتم تجميع برنامج Java الخاص بك من Java code إلى JVM bytecode ، وهو التمثيل الثنائي للبرنامج. إنه أكثر إحكاما وبساطة من كود مصدر Java. ومع ذلك ، لا يمكن فقط للمعالج العادي للكمبيوتر المحمول أو الخادم تنفيذ JVM bytecode.
لتشغيل البرنامج الخاص بك ، يفسر JVM هذا الرمز الفرعي. عادة ما يكون المترجمون أبطأ بكثير من رمز الجهاز الذي يعمل على المعالج. لهذا السبب ، يمكن لـ JVM ، أثناء تشغيل البرنامج ، تشغيل مترجم آخر يحول الرمز الفرعي الخاص بك إلى رمز الجهاز ، والذي يمكن للمعالج تنفيذه بالفعل.
هذا المترجم ، عادة ما يكون أكثر تعقيدًا من javac
، يقوم javac
معقدة لإنتاج كود آلة عالي الجودة نتيجة لذلك.
لماذا كتابة مترجم JIT في جافا؟
حتى الآن ، يتضمن تنفيذ JVM المسمى OpenJDK جامعي JIT رئيسيين. تم تصميم برنامج التحويل البرمجي الخاص بالعميل ، والمعروف باسم C1 ، للعمل بشكل أسرع ، ولكنه في نفس الوقت ينتج كودًا أقل تحسينًا. يتطلب المحول البرمجي للخادم ، المعروف باسم opto أو C2 ، مزيدًا من الوقت للعمل ، ولكنه ينتج كودًا أفضل.
كانت الفكرة هي أن برنامج التحويل البرمجي الخاص بالعميل كان مناسبًا بشكل أفضل لتطبيقات سطح المكتب ، حيث كانت فترات التوقف الطويلة في مترجم JIT غير مرغوب فيها ، وكان مترجم الخادم لتطبيقات الخادم التي تعمل لفترة طويلة ، والتي يمكن أن تقضي المزيد من الوقت في الترجمة.
اليوم يمكن دمجها بحيث يتم تجميع الشفرة أولاً بواسطة C1 ، ثم ، إذا استمر تنفيذها بشكل مكثف ومن المنطقي قضاء وقت إضافي ، - C2. وهذا ما يسمى التجميع المتدرج .
دعنا نتناول C2 ، مترجم الخادم الذي يؤدي المزيد من التحسينات.
يمكننا استنساخ OpenJDK من المرآة على GitHub ، أو فقط فتح شجرة المشروع على الموقع.
$ git clone https://github.com/dmlloyd/openjdk.git
كود C2 موجود في openjdk / hotspot / src / share / vm / opto .

بادئ ذي بدء ، تجدر الإشارة إلى أن C2 مكتوب بلغة C ++ . بالطبع ، هذا ليس شيئًا سيئًا ، ولكن هناك بعض العيوب. لغة C ++ هي لغة غير آمنة. هذا يعني أن الأخطاء في C ++ يمكن أن تحطم الجهاز الظاهري. ربما يكون السبب في ذلك هو عمر الرمز ، ولكن أصبح من الصعب جدًا الحفاظ على رمز C2 C ++ وتطويره.
قال كليف كليك ، أحد الشخصيات الرئيسية وراء مترجم C2 ، أنه لن يكتب مرة أخرى VM مرة أخرى في C ++ ، وسمعنا فريق Twitter JVM يقول أن C2 أصبح راكدًا ويجب استبداله لسبب صعوبات مواصلة التطوير.


https://www.youtube.com/watch؟v=Hqw57GJSrac
إذن ، بالعودة إلى السؤال ، ما هذا في Java الذي يمكن أن يساعد في حل هذه المشاكل؟ نفس الشيء الذي يعطي كتابة برنامج بلغة Java بدلاً من C ++. من المحتمل أن يكون هذا أمانًا (استثناءات بدلاً من الأعطال ، ولا يوجد تسرب حقيقي للذاكرة أو مؤشرات متدلية) ، ومساعدين جيدين ( مصححين ، وأدوات تحليل ، وأدوات مثل VisualVM ) ، ودعم IDE جيد ، إلخ.
قد تفكر: كيف يمكنني كتابة شيء مثل مترجم جافا JIT؟ ، وأن هذا ممكن فقط في لغة برمجة نظام منخفضة المستوى مثل C ++. في هذا العرض ، آمل أن أقنعك أن هذا ليس على الإطلاق! بشكل أساسي ، يجب على المترجم JIT فقط قبول JVM bytecode وإعطاء رمز الجهاز - يمكنك إعطاؤه byte[]
عند الإدخال ، وتريد أيضًا byte[]
. يتطلب الأمر الكثير من العمل المعقد للقيام بذلك ، ولكنه لا يؤثر على مستوى النظام ، وبالتالي لا يتطلب لغة نظام مثل C أو C ++.
إعداد Graal
أول شيء نحتاجه هو Java 9. تمت إضافة واجهة Graal المسماة JVMCI المستخدمة إلى Java كجزء من واجهة JEP 243 Java-Level JVM Compiler Interface ، والإصدار الأول الذي يتضمنها هو Java 9. أستخدم 9 + 181 . في حالة وجود أي متطلبات خاصة ، توجد منافذ (منافذ خلفية) لـ Java 8.
$ export JAVA_HOME=`pwd`/jdk9 $ export PATH=$JAVA_HOME/bin:$PATH $ java -version java version "9" Java(TM) SE Runtime Environment (build 9+181) Java HotSpot(TM) 64-Bit Server VM (build 9+181, mixed mode)
الشيء التالي الذي نحتاجه هو نظام بناء يسمى mx
. إنها تشبه إلى حد ما Maven أو Gradle ، ولكن على الأرجح لن تختارها لتطبيقك . يقوم بتنفيذ وظائف معينة لدعم بعض حالات الاستخدام المعقدة ، لكننا سنستخدمها فقط للتجميعات البسيطة.
يمكنك استنساخ mx
باستخدام GitHub. أنا أستخدم الالتزام #7353064
. الآن فقط قم بإضافة الملف التنفيذي إلى المسار.
$ git clone https://github.com/graalvm/mx.git $ cd mx; git checkout 7353064 $ export PATH=`pwd`/mx:$PATH
الآن نحن بحاجة إلى استنساخ Graal نفسه. أنا أستخدم توزيعة تسمى GraalVM الإصدار 0.28.2 .
$ git clone https://github.com/graalvm/graal.git --branch vm-enterprise-0.28.2
يحتوي هذا المستودع على مشاريع أخرى لا نهتم بها ، لذلك ننتقل فقط إلى المشروع الفرعي للمترجم ، وهو مترجم Graal JIT ، وتجميعه باستخدام mx
.
$ cd graal/compiler $ mx build
للعمل باستخدام كود Graal ، سأستخدم Eclipse IDE . أنا أستخدم Eclipse 4.7.1. يمكن أن يولد mx
ملفات مشروع Eclipse بالنسبة لنا.
$ mx eclipseinit
لفتح دليل الدرجات كمساحة عمل ، تحتاج إلى تنفيذ مشاريع ملف ، استيراد ... ، عام ، قائمة وتحديد دليل الدرجات مرة أخرى . إذا لم تقم بتشغيل Eclipse على Java 9 ، فقد تحتاج أيضًا إلى إرفاق مصادر JDK.

جيد. الآن بعد أن أصبح كل شيء جاهزًا ، دعنا نرى كيف يعمل. سنستخدم هذا الرمز البسيط للغاية.
class Demo { public static void main(String[] args) { while (true) { workload(14, 2); } } private static int workload(int a, int b) { return a + b; } }
أولاً ، نقوم بتجميع كود javac
هذا ، ثم تشغيل JVM. أولاً ، سأوضح لك كيف يعمل مترجم C2 JIT القياسي. للقيام بذلك ، -XX:+PrintCompilation
العديد من العلامات: -XX:+PrintCompilation
، وهو ضروري لـ JVM لكتابة سجل عند تجميع طريقة ، و -XX:CompileOnly=Demo::workload
، بحيث يتم تجميع هذه الطريقة فقط. إذا لم نفعل ذلك ، فسيتم عرض الكثير من المعلومات ، وسوف تكون JVM أكثر ذكاءً مما نحتاج إليه ، وستحسن الشفرة التي نريد رؤيتها.
$ javac Demo.java $ java \ -XX:+PrintCompilation \ -XX:CompileOnly=Demo::workload \ Demo ... 113 1 3 Demo::workload (4 bytes) ...
لن أشرح ذلك بالتفصيل ، لكنني سأقول فقط أن هذا ناتج سجل يوضح أن طريقة workload
تم تجميعها.
الآن ، بصفتنا مترجم JIT لـ Java 9 JVM ، نستخدم Graal المترجم حديثًا. للقيام بذلك ، أضف بعض العلامات الأخرى.
--module-path=...
and --upgrade-module-path=...
أضف Graal إلى مسار الوحدة . دعني أذكرك بأن مسار الوحدة النمطية ظهر في Java 9 كجزء من نظام وحدة Jigsaw ، ولأغراضنا يمكننا أن نأخذه في الاعتبار عن طريق القياس مع classpath .
نحتاج إلى -XX:+UnlockExperimentalVMOptions
بسبب حقيقة أن JVMCI (الواجهة المستخدمة من قبل Graal) في هذا الإصدار هي ميزة تجريبية.
العلامة -XX:+EnableJVMCI
مطلوبة للقول أننا نريد استخدام JVMCI ، و -XX:+UseJVMCICompiler
- لتمكين وتثبيت مترجم JIT جديد.
من أجل عدم تعقيد المثال ، وبدلاً من استخدام C1 بالاقتران مع JVMCI ، لديك فقط مترجم JVMCI ، حدد العلم -XX:-TieredCompilation
، والذي -XX:-TieredCompilation
.
كما كان من قبل ، نحدد العلامات -XX:+PrintCompilation
و -XX:CompileOnly=Demo::workload
.
كما في المثال السابق ، نرى أنه تم تصنيف طريقة واحدة. ولكن ، هذه المرة ، بالنسبة للتجميع ، استخدمنا للتو Graal. في الوقت الحالي ، خذ كلمتي فقط.
$ java \ --module-path=graal/sdk/mxbuild/modules/org.graalvm.graal_sdk.jar:graal/truffle/mxbuild/modules/com.oracle.truffle.truffle_api.jar \ --upgrade-module-path=graal/compiler/mxbuild/modules/jdk.internal.vm.compiler.jar \ -XX:+UnlockExperimentalVMOptions \ -XX:+EnableJVMCI \ -XX:+UseJVMCICompiler \ -XX:-TieredCompilation \ -XX:+PrintCompilation \ -XX:CompileOnly=Demo::workload \ Demo ... 583 25 Demo::workload (4 bytes) ...
واجهة مترجم JVM
ألا تعتقد أننا قمنا بشيء غير عادي؟ لدينا JVM مثبت ، واستبدلنا مترجم JIT مع المترجم الجديد الذي تم تجميعه للتو دون تغيير أي شيء في JVM نفسه. يتم توفير هذه الميزة من خلال واجهة JVM جديدة تسمى JVMCI ، واجهة مترجم JVM ، والتي ، كما قلت أعلاه ، كانت JEP 243 وجاءت في Java 9.
الفكرة مشابهة لبعض تقنيات JVM الموجودة الأخرى.
ربما صادفت javac
معالجة إضافية لشفرة المصدر في جافا javac
باستخدام واجهة برمجة تطبيقات معالجة التعليقات التوضيحية في جافا . تتيح هذه الآلية تحديد التعليقات التوضيحية ونموذج شفرة المصدر التي يتم استخدامها فيها ، وإنشاء ملفات جديدة بناءً عليها.
أيضا ، ربما تكون قد استخدمت معالجة كود إضافي في JVM باستخدام وكلاء Java . تتيح لك هذه الآلية تعديل Java bytecode عن طريق اعتراضها في وقت التمهيد.
فكرة JVMCI متشابهة. يسمح لك بتوصيل مترجم جافا JIT الخاص بك المكتوب بلغة جافا.
الآن أريد أن أقول بضع كلمات حول كيفية إظهار الرمز أثناء هذا العرض التقديمي. أولاً ، لفهم الفكرة ، سأعرض بعض المعرّفات المبسطة والمنطق في شكل نص على الشرائح ، وبعد ذلك سأنتقل إلى لقطات شاشة Eclipse وأعرض الشفرة الحقيقية ، والتي يمكن أن تكون أكثر تعقيدًا قليلاً ، لكن الفكرة الرئيسية ستظل كما هي. الغرض الرئيسي من هذا العرض التقديمي هو إظهار أنه من الممكن حقًا العمل مع رمز المشروع الحقيقي ، وبالتالي لا أريد إخفاءه ، على الرغم من أنه يمكن أن يكون معقدًا إلى حد ما.
من الآن فصاعدا ، أبدأ في تبديد الرأي القائل بأنه قد يكون لديك مجمع JIT معقد للغاية.
ماذا يقبل المترجم JIT للإدخال؟ يقبل الرمز الثانوي للطريقة التي سيتم تجميعها. والبايت كود ، كما يوحي الاسم ، هو مجرد مجموعة من البايتات.
ماذا ينتج مترجم JIT نتيجة لذلك؟ يعطي رمز الجهاز للطريقة. رمز الجهاز هو أيضًا مجموعة من وحدات البايت.
نتيجة لذلك ، ستبدو الواجهة التي يجب تنفيذها عند كتابة مترجم JIT جديد لتضمينه في JVM مثل هذا.
interface JVMCICompiler { byte[] compileMethod(byte[] bytecode); }
لذلك ، إذا لم تستطع أن تتخيل كيف يمكن لـ Java القيام بشيء منخفض المستوى مثل تجميع JIT في رمز الآلة ، يمكنك الآن أن ترى أن هذا ليس عملًا منخفض المستوى. صحيح؟ هذه مجرد دالة من byte[]
إلى byte[]
.
في الواقع ، كل شيء أكثر تعقيدًا إلى حد ما. مجرد بايت كود ليس كافيا - نحن بحاجة أيضا إلى مزيد من المعلومات ، مثل عدد المتغيرات المحلية ، وحجم المكدس المطلوب ، والمعلومات التي تم جمعها من قبل ملف تعريف المترجم لفهم كيفية تنفيذ الشفرة في الواقع. لذلك ، تخيل الإدخال في شكل CompilationRequest
، والذي سيخبرنا عن JavaMethod
الذي يجب JavaMethod
وتوفير جميع المعلومات اللازمة.
interface JVMCICompiler { void compileMethod(CompilationRequest request); } interface CompilationRequest { JavaMethod getMethod(); } interface JavaMethod { byte[] getCode(); int getMaxLocals(); int getMaxStackSize(); ProfilingInfo getProfilingInfo(); ... }
أيضا ، لا تتطلب الواجهة إرجاع التعليمات البرمجية المترجمة. بدلاً من ذلك ، يتم استخدام API أخرى لتثبيت رمز الجهاز في JVM.
HotSpot.installCode(targetCode);
الآن ، لكتابة مترجم JIT جديد لـ JVM ، ما عليك سوى تنفيذ هذه الواجهة. نحصل على معلومات حول الطريقة التي يجب installCode
، ويجب أن نقوم بتجميعها في رمز الجهاز واستدعاء installCode
.
class GraalCompiler implements JVMCICompiler { void compileMethod(CompilationRequest request) { HotSpot.installCode(...); } }
دعنا ننتقل إلى Eclipse IDE مع Graal وإلقاء نظرة على بعض الواجهات والفئات الحقيقية. كما ذكرنا سابقًا ، سيكونون أكثر تعقيدًا إلى حد ما ، ولكن ليس كثيرًا.



الآن أريد أن أثبت أنه يمكننا إجراء تغييرات على Graal ، واستخدامها على الفور في Java 9. سأضيف رسالة سجل جديدة سيتم عرضها عند تجميع الطريقة باستخدام Graal. قم بإضافته إلى طريقة الواجهة المطبقة ، والتي يطلق عليها JVMCI.
class HotSpotGraalCompiler implements JVMCICompiler { CompilationRequestResult compileMethod(CompilationRequest request) { System.err.println("Going to compile " + request.getMethod().getName()); ... } }

الآن ، قم بتعطيل تسجيل الترجمة في HotSpot. الآن يمكننا رؤية رسالتنا من النسخة المعدلة للمترجم.
$ java \ --module-path=graal/sdk/mxbuild/modules/org.graalvm.graal_sdk.jar:graal/truffle/mxbuild/modules/com.oracle.truffle.truffle_api.jar \ --upgrade-module-path=graal/compiler/mxbuild/modules/jdk.internal.vm.compiler.jar \ -XX:+UnlockExperimentalVMOptions \ -XX:+EnableJVMCI \ -XX:+UseJVMCICompiler \ -XX:-TieredCompilation \ -XX:CompileOnly=Demo::workload \ Demo Going to compile workload
إذا حاولت تكرار ذلك بنفسك ، ستلاحظ أنك لست بحاجة حتى إلى تشغيل نظام البناء لدينا - mx build
. يكفي ، عادي لـ Eclipse ، تجميع عند الحفظ . وبالتأكيد لا نحتاج إلى إعادة بناء الآلية المشتركة نفسها. نقوم ببساطة بتضمين المترجم المعدل في JVM الحالي.
الكونت جراال
حسنًا ، نحن نعلم أن Graal يحول byte[]
واحدًا byte[]
إلى byte[]
آخر byte[]
. الآن دعونا نتحدث عن النظرية وهياكل البيانات التي يستخدمها ، لأنه فهي غير عادية بعض الشيء حتى بالنسبة للمترجم.
بشكل أساسي ، يعالج المترجم برنامجك. لهذا ، يجب تقديم البرنامج في شكل نوع من هيكل البيانات. أحد الخيارات هو البايت كود وقوائم التعليمات المشابهة ، لكنها ليست معبرة جدًا.
بدلاً من ذلك ، يستخدم Graal رسمًا بيانيًا لتمثيل برنامجك. إذا أخذنا عامل إضافة بسيطًا يلخص متغيرين محليين ، فسيشتمل الرسم البياني على عقدة واحدة لتحميل كل متغير ، وعقدة واحدة للمجموع ، وحافتين تظهران أن نتيجة تحميل المتغيرات المحلية هي إدخال لعامل الإضافة.
يُسمى هذا أحيانًا الرسم البياني لتبعية البرنامج .
بالحصول على تعبير عن النموذج x + y
نحصل على عقد للمتغيرات المحلية x
و y
، وعقدة من مجموعها.

توضح الحواف الزرقاء في هذا الرسم البياني اتجاه تدفق البيانات من قراءة المتغيرات المحلية إلى الجمع.
أيضا ، يمكننا استخدام الحواف لتعكس ترتيب التنفيذ للبرنامج. إذا ، بدلاً من قراءة المتغيرات المحلية ، اتصلنا بالأساليب ، فإننا بحاجة إلى تذكر ترتيب المكالمة ، ولا يمكننا إعادة ترتيبها (دون معرفة الرمز الموجود في الداخل). للقيام بذلك ، هناك حواف إضافية تحدد هذا الترتيب. تظهر باللون الأحمر.

إذن ، الرسم البياني Graal ، في الواقع ، هو رسمان بيانيان مدمجان في واحد. العقد هي نفسها ، ولكن بعض الحواف تشير إلى اتجاه دفق البيانات ، في حين أن البعض الآخر يظهر نقل التحكم بينهما.
لرؤية الرسم البياني Graal ، يمكنك استخدام أداة تسمى IdealGraphVisualiser أو IGV . يتم بدء التشغيل باستخدام mx igv
.

بعد ذلك ، قم بتشغيل JVM -Dgraal.Dump
.
يمكن رؤية دفق بيانات بسيط عن طريق كتابة تعبير بسيط.
int average(int a, int b) { return (a + b) / 2; }

يمكنك أن ترى كيف تذهب المعلمات 0
( P(0
) و 1
( P(1)
) إلى إدخال عملية الإضافة ، والتي ، مع الثابت 2
( C(2)
) تذهب إلى إدخال عملية القسمة ، وبعد ذلك يتم إرجاع هذه القيمة.
من أجل النظر إلى تدفق أكثر تعقيدًا للبيانات والتحكم ، نقدم دورة.
int average(int[] values) { int sum = 0; for (int n = 0; n < values.length; n++) { sum += values[n]; } return sum / values.length; }


في هذه الحالة ، لدينا عقد بداية ونهاية الحلقة ، وقراءة عناصر المصفوفة ، وقراءة طول المصفوفة. كما كان من قبل ، تشير الخطوط الزرقاء إلى اتجاه تدفق البيانات ، وتشير الخطوط الحمراء إلى تدفق التحكم.
يمكنك الآن معرفة سبب تسمية بنية البيانات أحيانًا بحر العقد أو حساء العقد .
أريد أن أقول أن C2 تستخدم بنية بيانات متشابهة جدًا ، وفي الواقع ، كانت C2 هي التي شاعت فكرة مترجم بحر من العقد ، لذلك هذا ليس ابتكارًا لـ Graal.
لن أعرض عملية إنشاء هذا الرسم البياني حتى الجزء التالي من العرض التقديمي ، ولكن عندما يتلقى Graal البرنامج بهذا التنسيق ، يتم إجراء التحسين والتجميع عن طريق تعديل بنية البيانات هذه. وهذا أحد الأسباب التي تجعل كتابة مترجم JIT في Java منطقية. Java هي لغة موجهة للكائنات ، والرسم البياني عبارة عن مجموعة من الكائنات المتصلة بواسطة حواف في شكل روابط.
من الرمز البايت إلى رمز الجهاز
دعونا نرى كيف تبدو هذه الأفكار في الممارسة ، ونتبع بعض خطوات عملية التجميع.
الحصول على Bytecode
يبدأ التجميع بالبايت كود. دعونا نعود إلى مثال التلخيص الصغير.
int workload(int a, int b) { return a + b; }
سنقوم بإخراج الرمز الثانوي المستلم عند الإدخال قبل بدء التجميع.
class HotSpotGraalCompiler implements JVMCICompiler { CompilationRequestResult compileMethod(CompilationRequest request) { System.err.println(request.getMethod().getName() + " bytecode: " + Arrays.toString(request.getMethod().getCode())); ... } }
workload bytecode: [26, 27, 96, -84]
كما ترون ، فإن المدخل إلى المجمع هو bytecode.
محلل Bytecode ومنشئ الرسم البياني
يقوم المُنشئ ، إذ يدرك هذا الصفيف من البايت بأنه كود بايت JVM ، بتحويله إلى رسم بياني Graal. هذا نوع من التفسير المجرد - يفسر الباني كود Java bytecode ، ولكن بدلاً من تمرير القيم ، يعالج النهايات الحرة للحواف ويربطها ببعضها البعض تدريجيًا.
دعونا نستفيد من حقيقة أن Graal مكتوب بلغة Java ونرى كيف يعمل باستخدام أدوات التنقل Eclipse. نحن نعلم أن هناك عقدة إضافة في مثالنا ، لذلك دعونا نجد مكان إنشائها.



يمكن ملاحظة أنه تم إنشاؤها بواسطة محلل IADD
كود ، وهذا قادنا إلى IADD
معالجة IADD
( 96
، الذي رأيناه في صفيف الإدخال المطبوع).
private void genArithmeticOp(JavaKind kind, int opcode) { ValueNode y = frameState.pop(kind); ValueNode x = frameState.pop(kind); ValueNode v; switch (opcode) { ... case LADD: v = genIntegerAdd(x, y); break; ... } frameState.push(kind, append(v)); }
قلت أعلاه إن هذا تفسير مجرد ، لأنه كل هذا يشبه إلى حد بعيد مترجم الشفرة الثانوية. إذا كان مترجمًا حقيقيًا لـ JVM ، فسيأخذ قيمتين من المكدس ، ويؤدي الإضافة ، ويعيد النتيجة. في هذه الحالة ، نقوم بإزالة عقدتين من المكدس ، والتي ، عند بدء البرنامج ، ستكون حسابات ، نضيف ، والتي هي نتيجة الجمع ، عقدة جديدة للإضافة ، ونضعها على المكدس.
وهكذا تم بناء الرسم البياني Graal.
الحصول على رمز الجهاز
لتحويل الرسم البياني Graal إلى رمز الجهاز ، تحتاج إلى إنشاء وحدات البايت لكل عقده. يتم ذلك بشكل منفصل لكل عقدة عن طريق استدعاء أسلوب generate
الخاص بها.
void generate(Generator gen) { gen.emitAdd(a, b); }
أكرر ، هنا نعمل بمستوى عالٍ جدًا من التجريد. لدينا فئة نصدر بها تعليمات رمز الجهاز دون الخوض في تفاصيل كيفية عمل ذلك.
emitAdd
, , , , . .
int workload(int a) { return a + 1; }
, .
void incl(Register dst) { int encode = prefixAndEncode(dst.encoding); emitByte(0xFF); emitByte(0xC0 | encode); } void emitByte(int b) { data.put((byte) (b & 0xFF)); }


, , ByteBuffer
— .
— .
class HotSpotGraalCompiler implements JVMCICompiler { CompilationResult compileHelper(...) { ... System.err.println(method.getName() + " machine code: " + Arrays.toString(result.getTargetCode())); ... } }

. HotSpot. . OpenJDK, , -, JVM, .
$ cd openjdk/hotspot/src/share/tools/hsdis $ curl -O http://ftp.heanet.ie/mirrors/gnu/binutils/binutils-2.24.tar.gz $ tar -xzf binutils-2.24.tar.gz $ make BINUTILS=binutils-2.24 ARCH=amd64 CFLAGS=-Wno-error $ cp build/macosx-amd64/hsdis-amd64.dylib ../../../../../..
: -XX:+UnlockDiagnosticVMOptions
-XX:+PrintAssembly
.
$ java \ --module-path=graal/sdk/mxbuild/modules/org.graalvm.graal_sdk.jar:graal/truffle/mxbuild/modules/com.oracle.truffle.truffle_api.jar \ --upgrade-module-path=graal/compiler/mxbuild/modules/jdk.internal.vm.compiler.jar \ -XX:+UnlockExperimentalVMOptions \ -XX:+EnableJVMCI \ -XX:+UseJVMCICompiler \ -XX:-TieredCompilation \ -XX:+PrintCompilation \ -XX:+UnlockDiagnosticVMOptions \ -XX:+PrintAssembly \ -XX:CompileOnly=Demo::workload \ Demo
.
workload machine code: [15, 31, 68, 0, 0, 3, -14, -117, -58, -123, 5, ...] ... 0x000000010f71cda0: nopl 0x0(%rax,%rax,1) 0x000000010f71cda5: add %edx,%esi ;\*iadd {reexecute=0 rethrow=0 return_oop=0} ; - Demo::workload@2 (line 10) 0x000000010f71cda7: mov %esi,%eax ;\*ireturn {reexecute=0 rethrow=0 return_oop=0} ; - Demo::workload@3 (line 10) 0x000000010f71cda9: test %eax,-0xcba8da9(%rip)
. , . generate
, .
class AddNode { void generate(...) { ... gen.emitSub(op1, op2, false) ...

, , , .
workload mechine code: [15, 31, 68, 0, 0, 43, -14, -117, -58, -123, 5, ...] 0x0000000107f451a0: nopl 0x0(%rax,%rax,1) 0x0000000107f451a5: sub %edx,%esi ;\*iadd {reexecute=0 rethrow=0 return_oop=0} ; - Demo::workload@2 (line 10) 0x0000000107f451a7: mov %esi,%eax ;\*ireturn {reexecute=0 rethrow=0 return_oop=0} ; - Demo::workload@3 (line 10) 0x0000000107f451a9: test %eax,-0x1db81a9(%rip)
, ? Graal ; ; ; . , Graal.
[26, 27, 96, -84] → [15, 31, 68, 0, 0, 43, -14, -117, -58, -123, 5, ...]
, , . Graal , .
— . .
interface Phase { void run(Graph graph); }
(canonicalisation)
. , , ( constant folding ) .
— canonical
.
interface Node { Node canonical(); }
, , , . — . -(-x)
x
.
class NegateNode implements Node { Node canonical() { if (value instanceof NegateNode) { return ((NegateNode) value).getValue(); } else { return this; } } }

Graal . , .
Java, canonical
.
Global value numbering
Global value numbering (GVN) — . a + b
, — .
int workload(int a, int b) { return (a + b) * (a + b); }
Graal . — . GVN . hash map , , .


, — , , - . , , , , — .
int workload() { return (getA() + getB()) * (getA() + getB()); }

(lock coarsening)
. . , , , ( inlining ).
void workload() { synchronized (monitor) { counter++; } synchronized (monitor) { counter++; } }
, , , , .
void workload() { monitor.enter(); counter++; monitor.exit(); monitor.enter(); counter++; monitor.exit(); }
. .
void workload() { monitor.enter(); counter++; counter++; monitor.exit(); }
Graal LockEliminationPhase
. run
, . , , .
void run(StructuredGraph graph) { for (monitorExitNode monitorExitNode : graph.getNodes(MonitorExitNode.class)) { FixedNode next = monitorExitNode.next(); if (next instanceof monitorEnterNode) { AccessmonitorNode monitorEnterNode = (AccessmonitorNode) next; if (monitorEnterNode.object() ## monitorExitNode.object()) { monitorExitNode.remove(); monitorEnterNode.remove(); } } } }

, , , , 2
.
void workload() { monitor.enter(); counter += 2; monitor.exit(); }
IGV . , , \ , , , 2
.


Graal , , , . , , , .
Graal , , , , , , .
Graal , , . ? , ?
, , . . , , , , . , , , , , .
( register allocation ). Graal , JIT-, — ( linear scan algorithm ).
, , , - , .
, , , , (.. ), . , , .
( graph scheduling ). . , . , , , .
, .
Graal?
, , , Graal — , Oracle . , Graal?
(final-tier compiler)
C JVMCI Graal HotSpot — , . ( HotSpot) Graal , .
Twitter Graal , Java 9 . : -XX:+UseJVMCICompiler
.
JVMCI , Graal JVM. (deploy) - JVM, Graal. Java-, Graal, JVM.
OpenJDK Metropolis JVM Java. Graal .

http://cr.openjdk.java.net/\~jrose/metropolis/Metropolis-Proposal.html
Graal . Graal JVM, Graal . , Graal . , - , , JNI.
Charles Nutter JRuby Graal Ruby. , - .
AOT (ahead-of-time)
Graal — Java. JVMCI , Graal, , , Graal . , Graal , JIT-.
JIT- AOT- , Graal . AOT Graal.
Java 9 JIT-, . JVM, .
AOT Java 9 Graal, Linux. , , .
. SubstrateVM — AOT-, Java- JVM . , - (statically linked) . JVM , . SubstrateVM Graal. ( just-in-time ) SubstrateVM, , Graal . Graal AOT- .
$ javac Hello.java $ graalvm-0.28.2/bin/native-image Hello classlist: 966.44 ms (cap): 804.46 ms setup: 1,514.31 ms (typeflow): 2,580.70 ms (objects): 719.04 ms (features): 16.27 ms analysis: 3,422.58 ms universe: 262.09 ms (parse): 528.44 ms (inline): 1,259.94 ms (compile): 6,716.20 ms compile: 8,817.97 ms image: 1,070.29 ms debuginfo: 672.64 ms write: 1,797.45 ms [total]: 17,907.56 ms $ ls -lh hello -rwxr-xr-x 1 chrisseaton staff 6.6M 4 Oct 18:35 hello $ file ./hello ./hellojava: Mach-O 64-bit executable x86_64 $ time ./hello Hello! real 0m0.010s user 0m0.003s sys 0m0.003s
Truffle
Graal Truffle . Truffle — JVM.
, JVM, , JIT- (, , , JIT- JVM , ). Truffle — , , Truffle, , ( partial evaluation ).
, ( inlining ) ( constant folding ) . Graal , Truffle .
Graal — Truffle. Ruby, TruffleRuby Truffle , , Graal. TruffleRuby — Ruby, 10 , , , .
https://github.com/graalvm/truffleruby
الاستنتاجات
, , , JIT- Java . JIT- , , , - . , , . JIT- , byte[]
JVM byte[]
.
, Java. , C++.
Java- Graal - . , , .
. , Eclipse . (definitions), .. .
JIT JIT- JVM, JITWatch , , Graal , . , , - , Graal JVM. IDE, hello-world .
SubstrateVM Truffle, Graal, , Java . , Graal Java. , , - LLVM , , , , .
, , Graal JVM. لأن JVMCI Java 9, Graal , , Java-.
Graal — . , Graal. , !
More information about TruffleRuby
Low Overhead Polling For Ruby
Top 10 Things To Do With GraalVM
Ruby Objects as C Structs and Vice Versa
Understanding How Graal Works — a Java JIT Compiler Written in Java
Flip-Flops — the 1-in-10-million operator
Deoptimizing Ruby
Very High Performance C Extensions For JRuby+Truffle
Optimising Small Data Structures in JRuby+Truffle
Pushing Pixels with JRuby+Truffle
Tracing With Zero Overhead in JRuby+Truffle
How Method Dispatch Works in JRuby+Truffle
A Truffle/Graal High Performance Backend for JRuby