مرحبا بالجميع!
اليوم ، يتم توجيه انتباهك إلى ترجمة المقالة ، والتي تعرض أمثلة على خيارات الترجمة في JVM. يتم إيلاء اهتمام خاص لمجموعة AOT المدعومة في Java 9 والإصدارات الأحدث.
هل لديك قراءة لطيفة!
أفترض أن أي شخص قام ببرمجة في Java قد سمع عن الترجمة الفورية (JIT) ، وربما التجميع قبل التنفيذ (AOT). بالإضافة إلى ذلك ، ليست هناك حاجة لشرح اللغات "المفسرة". تشرح هذه المقالة كيفية تنفيذ كل هذه الميزات في جهاز Java الظاهري ، JVM.
ربما تعلم أنه عند البرمجة في Java ، ستحتاج إلى تشغيل مترجم (باستخدام برنامج "javac") الذي يجمع شفرة مصدر Java (ملفات .java) في Java bytecode (ملفات .class). جافا بايت هو لغة وسيطة. يطلق عليه "متوسط" لأنه لا يفهمه جهاز حوسبة حقيقي (CPU) ولا يمكن تنفيذه بواسطة جهاز كمبيوتر ، وبالتالي ، يمثل نموذجًا انتقاليًا بين الكود المصدر ورمز الجهاز "الأصلي" الذي تم تنفيذه في المعالج.
لكي يقوم Java bytecode بأداء أي عمل محدد ، هناك 3 طرق للحصول عليه للقيام بذلك:
- مباشرة تنفيذ التعليمات البرمجية الوسيطة. من الأفضل والأكثر صحة القول أنه يحتاج إلى "تفسير". JVM لديه مترجم جافا. كما تعلمون ، لكي تعمل JVM ، فأنت بحاجة إلى تشغيل برنامج "java".
- قبل تنفيذ الشفرة الوسيطة ، قم بتجميعها إلى رمز أصلي واجبر وحدة المعالجة المركزية على تنفيذ هذا الكود الأصلي الطازج. وبالتالي ، يتم التجميع قبل التنفيذ مباشرة (فقط في الوقت المناسب) ويسمى "ديناميكي".
- 3 أول شيء ، حتى قبل إطلاق البرنامج ، يتم ترجمة الشفرة الوسيطة إلى لغة أصلية وتشغيلها من خلال وحدة المعالجة المركزية من البداية إلى النهاية. يتم هذا التجميع قبل التنفيذ ويسمى AoT (Ahead of Time).
لذلك ، (1) هو عمل المترجم الفوري ، (2) هو نتيجة تجميع JIT ، و (3) هو نتيجة تجميع AOT.
من أجل الاكتمال ، سأذكر أن هناك طريقة رابعة - لتفسير الكود المصدري مباشرةً ، لكن هذا في Java غير مقبول. يتم ذلك ، على سبيل المثال ، في بيثون.
الآن دعنا نرى كيف يعمل "java" كـ (1) مترجم (2) مترجم JIT و / أو (3) مترجم AOT - ومتى.
باختصار - كقاعدة عامة ، "java" تقوم بكل من (1) و (2). بدءًا من Java 9 ، هناك خيار ثالث ممكن أيضًا.
هنا فئة
Test
بنا ، والتي سيتم استخدامها في الأمثلة المستقبلية.
public class Test { public int f() throws Exception { int a = 5; return a; } public static void main(String[] args) throws Exception { for (int i = 1; i <= 10; i++) { System.out.println("call " + Integer.valueOf(i)); long a = System.nanoTime(); new Test().f(); long b = System.nanoTime(); System.out.println("elapsed= " + (ba)); } } }
كما ترون ، هناك طريقة
main
تعمل على إنشاء كائن
Test
وتستدعي الدالة
f
بشكل دوري 10 مرات على التوالي. وظيفة
f
لا تفعل شيئا تقريبا.
لذلك ، إذا قمت بترجمة وتشغيل الشفرة أعلاه ، فسيكون الإخراج متوقعًا تمامًا (بالطبع ، ستختلف قيم الوقت المنقضي عنك):
call 1 elapsed= 5373 call 2 elapsed= 913 call 3 elapsed= 654 call 4 elapsed= 623 call 5 elapsed= 680 call 6 elapsed= 710 call 7 elapsed= 728 call 8 elapsed= 699 call 9 elapsed= 853 call 10 elapsed= 645
والسؤال المطروح الآن هو: هل هذا الاستنتاج هو نتيجة عمل "java" كمترجم ، أي الخيار (1) ، "java" كمترجم JIT ، أي الخيار (2) أو هل هو مرتبط بطريقة ما بتجميع AOT ، وهذا هو ، الخيار (3)؟ في هذه المقالة سأجد الإجابات الصحيحة على كل هذه الأسئلة.
الإجابة الأولى التي أريد تقديمها هي على الأرجح (1) فقط تحدث هنا. أقول "على الأرجح" ، لأنني لا أعرف إذا تم تعيين أي متغير بيئة هنا من شأنه تغيير خيارات JVM الافتراضية. إذا لم يتم تثبيت أي شيء لا لزوم له ، وهذه هي الطريقة التي يعمل بها "java" افتراضيًا ، فنحن هنا نراقب الخيار فقط بنسبة 100٪ ، أي يتم تفسير الشفرة بالكامل. أنا متأكد من هذا ، منذ:
- وفقًا لوثائق java ، يتم تشغيل
-XX:CompileThreshold=invocations
مع invocations=1500
الافتراضية invocations=1500
على JVM للعميل (يتم شرح المزيد عن العميل JVM أدناه). منذ أن قمت بتشغيله 10 مرات فقط و 10 <1500 ، لا نتحدث عن تجميع ديناميكي هنا. عادةً ما يحدد خيار سطر الأوامر هذا عدد المرات (الحد الأقصى) التي يجب أن يتم تفسير الوظيفة قبل بدء خطوة التحويل الديناميكي. سوف أسهب في هذا أدناه. - في الواقع ، قمت بتشغيل هذا الرمز مع أعلام التشخيص ، لذلك أعرف ما إذا كان تم تجميعه ديناميكيًا. سأشرح أيضًا هذه النقطة أدناه.
يرجى ملاحظة: JVM يمكن أن تعمل في وضع العميل أو الخادم ، والخيارات التي تم تعيينها بشكل افتراضي في الحالتين الأولى والثانية ستكون مختلفة. كقاعدة عامة ، يتم اتخاذ القرار بشأن وضع بدء التشغيل تلقائيًا ، اعتمادًا على البيئة أو الكمبيوتر حيث تم تشغيل JVM. فيما يلي ،
–client
خيار
–client
خلال جميع عمليات التشغيل ، حتى لا أشك في أن البرنامج يعمل في وضع العميل. لن يؤثر هذا الخيار على الجوانب التي أريد إظهارها في هذا المنشور.
إذا قمت بتشغيل "java" باستخدام
-XX:PrintCompilation
، فسيقوم البرنامج بطباعة سطر عند تجميع الوظيفة ديناميكيًا. لا تنس أن يتم إجراء ترجمة JIT لكل وظيفة على حدة ، فقد تظل بعض الوظائف في الفصل في رمز ثانوي (أي ، ليس مترجم) ، في حين أن البعض الآخر قد اجتاز بالفعل ترجمة JIT ، وهذا جاهز للتنفيذ المباشر في المعالج .
أدناه أنا أيضا إضافة خيار
-Xbatch
. هناك حاجة إلى خيار
-Xbatch
فقط لجعل الإخراج أكثر ملاءمة. بخلاف ذلك ، يستمر تجميع JIT بشكل تنافسي (إلى جانب التفسير) ، وقد يبدو الإخراج بعد
-XX:PrintCompilation
في بعض الأحيان غريبًا في وقت التشغيل (بسبب -
-XX:PrintCompilation
). ومع ذلك ، فإن الخيار
–Xbatch
يعطل ترجمة الخلفية ، وبالتالي ، قبل تنفيذ ترجمة JIT ، سيتم إيقاف تنفيذ برنامجنا.
(من أجل سهولة القراءة ، سأكتب كل خيار من سطر جديد)
$ java -client -Xbatch -XX:+PrintCompilation Test
لن أدرج مخرجات هذا الأمر هنا ، لأن JVM يجمع العديد من الوظائف الداخلية افتراضيًا (مثل ، على سبيل المثال ، حزم java و sun و jdk) ، وبالتالي فإن الإخراج سيكون طويل جدًا - لذلك ، على شاشتي ، هناك 274 سطرًا في الوظائف الداخلية ، وأكثر من ذلك - إلى غاية البرنامج). لتسهيل هذا البحث ، سأقوم بإلغاء تجميع JIT للفئات الداخلية أو تمكينه بشكل انتقائي فقط
Test.f
(
Test.f
). للقيام بذلك ، حدد خيارًا آخر ،
-XX:CompileCommand
. يمكنك تحديد العديد من الأوامر (التحويل البرمجي) ، لذلك سيكون من الأسهل وضعها في ملف منفصل. لحسن الحظ ، لدينا خيار
-XX:CompileCommandFile
. لذلك ، انتقل إلى إنشاء الملف. سوف أسميها
hotspot_compiler
لسبب
hotspot_compiler
قريبًا وأكتب ما يلي:
quiet exclude java/* * exclude jdk/* * exclude sun/* *
في هذه الحالة ، يجب أن يكون من الواضح تمامًا أننا نستبعد جميع الوظائف (الأخيرة *) في جميع الفئات من جميع الحزم التي تبدأ بـ java و jdk و sun (يتم فصل أسماء الحزم بـ / ، ويمكنك استخدام *). يخبر الأمر
quiet
JVM بعدم كتابة أي شيء عن الفئات المستبعدة ، لذلك فقط تلك التي يتم تجميعها الآن سيتم إخراجها إلى وحدة التحكم. لذلك ، أركض:
java -client -Xbatch -XX:+PrintCompilation -XX:CompileCommandFile=hotspot_compiler Test
قبل إخبارك بإخراج هذا الأمر ، أذكرك بأنني قمت بتسمية هذا الملف
hotspot_compiler
، لأنه يبدو (لم
.hotspot_compiler
) أنه في Oracle JDK ، يتم تعيين الاسم
.hotspot_compiler
افتراضيًا للملف باستخدام أوامر برنامج التحويل البرمجي.
لذلك الاستنتاج هو:
many lines like this 111 1 n 0 java.lang.invoke.MethodHandle::linkToStatic(LLLLLL)L (native) (static) call 1 some more lines like this 161 48 n 0 java.lang.invoke.MethodHandle::linkToStatic(ILIJL)I (native) (static) elapsed= 7558 call 2 elapsed= 1532 call 3 elapsed= 920 call 4 elapsed= 732 call 5 elapsed= 774 call 6 elapsed= 815 call 7 elapsed= 767 call 8 elapsed= 765 call 9 elapsed= 757 call 10 elapsed= 868
أولاً ، لا أعرف لماذا لا تزال بعض أساليب
java.lang.invoke.MethodHandler.
ربما ، بعض الأشياء فقط لا يمكن إيقاف. كما فهمت ما الأمر ، فسوف أقوم بتحديث هذا المنشور. ومع ذلك ، كما ترون ، فقد اختفت الآن جميع خطوات الترجمة (سابقًا كانت هناك 274 سطرًا). في أمثلة أخرى ، سأقوم أيضًا بإزالة
java.lang.invoke.MethodHandler
من إخراج سجل التحويل البرمجي.
دعونا نرى ما وصلنا إليه. الآن لدينا رمز بسيط حيث ندير وظيفتنا 10 مرات. لقد ذكرت سابقًا أن هذه الوظيفة يتم تفسيرها ، وليس تجميعها ، كما هو موضح في الوثائق ، والآن نراها في السجلات (في الوقت نفسه ، لا نراها في سجلات الترجمة ، وهذا يعني أنها لا تخضع لتجميع JIT). حسنًا ، لقد رأيت للتو أداة "java" تعمل ، وتفسير وتفسير وظيفتنا فقط في 100٪ من الحالات. لذلك ، يمكننا تحديد المربع الذي برز بالخيار (1). نمر إلى (2) ، تجميع ديناميكي.
وفقًا للوثائق ، يمكنك تشغيل الوظيفة 1500 مرة والتأكد من أن تجميع JIT يحدث بالفعل. ومع ذلك ، يمكنك أيضًا استخدام
-XX:CompileThreshold=invocations
استدعاء
-XX:CompileThreshold=invocations
، وتحديد القيمة المطلوبة بدلاً من 1500. دعنا نشير هنا 5. هذا يعني أننا نتوقع ما يلي: بعد 5 "تفسيرات" من وظيفتنا f ، يجب على JVM ترجمة الطريقة ، ثم تشغيل الإصدار المترجم.
جافا-العميل -Xbatch
-XX:+PrintCompilation -XX:CompileCommandFile=hotspot_compiler -XX:CompileThreshold=5 Test
إذا قمت بتشغيل هذا الأمر ، فربما لاحظت أنه لم يتغير شيء مقارنة بالمثال أعلاه. وهذا هو ، لا يزال لا يحدث تجميع. اتضح ، وفقًا للوثائق ،
-XX:CompileThreshold
لا يعمل إلا عند تعطيل
TieredCompilation
، وهو الافتراضي. يتم
-XX:-TieredCompilation
مثل هذا:
-XX:-TieredCompilation
. Tiered Compilation هي ميزة تم تقديمها في Java 7 لتحسين كل من إطلاق وسرعة الانطلاق في JVM. في سياق هذا المنشور ، ليس من المهم ، لذلك لا تتردد في تعطيله. لنقم الآن بتشغيل هذا الأمر مرة أخرى:
java -client -Xbatch -XX:+PrintCompilation -XX:CompileCommandFile=hotspot_compiler -XX:CompileThreshold=5 -XX:-TieredCompilation Test
هنا هو الإخراج (أذكر ، لقد فاتني الأسطر المتعلقة
java.lang.invoke.MethodHandle
):
call 1 elapsed= 9411 call 2 elapsed= 1291 call 3 elapsed= 862 call 4 elapsed= 1023 call 5 227 56 b Test::<init> (5 bytes) 228 57 b Test::f (4 bytes) elapsed= 1051739 call 6 elapsed= 18516 call 7 elapsed= 940 call 8 elapsed= 769 call 9 elapsed= 855 call 10 elapsed= 838
نرحب (مرحبًا!) الدالة المترجمة ديناميكيًا Test.f أو
Test::<init>
مباشرةً بعد الاتصال بالرقم 5 ، لأنني قمت بتعيين CompileThreshold على 5. تفسر JVM الوظيفة 5 مرات ، ثم تقوم بترجمتها ثم تشغيل الإصدار المترجم أخيرًا. نظرًا لأن الوظيفة يتم تصنيفها ، يجب أن تعمل بشكل أسرع ، لكن لا يمكننا التحقق من ذلك هنا ، لأن هذه الوظيفة لا تفعل شيئًا. أعتقد أن هذا موضوع جيد لوظيفة منفصلة.
كما قد تكون خمنت بالفعل ، يتم تجميع وظيفة أخرى هنا ، وهي
Test::<init>
، وهو مُنشئ لفئة
Test
. نظرًا لأن الرمز يستدعي المُنشئ (
Test()
الجديد) ، فكلما
f
استدعاء
f
، يتم تجميعه في نفس الوقت مع الدالة
f
، بعد 5 مكالمات تمامًا.
من حيث المبدأ ، يمكن أن ينهي هذا مناقشة الخيار (2) ، تجميع JIT. كما ترون ، في هذه الحالة ، يتم تفسير الوظيفة أولاً بواسطة JVM ، ثم يتم تجميعها ديناميكيًا بعد تفسير خمسة أضعاف. أرغب في إضافة التفاصيل الأخيرة المتعلقة
-XX:+PrintAssembly
JIT ، أي ذكر الخيار
-XX:+PrintAssembly
. كما يوحي الاسم ، فإنه يخرج إلى وحدة التحكم نسخة مترجمة من الوظيفة (إصدار مترجم = كود الجهاز الأصلي = رمز المجمع). ومع ذلك ، لن يعمل هذا إلا إذا كان هناك مفكك في مسار المكتبة. أعتقد أن disassembler قد يختلف في JVMs مختلفة ، ولكن في هذه الحالة نحن نتعامل مع hsdis - disassembler for openjdk. يمكن الحصول على الكود المصدري لمكتبة hsdis أو ملفها الثنائي في أماكن مختلفة. في هذه الحالة ، قمت بتجميع هذا الملف ووضع
hsdis-amd64.so
في
JAVA_HOME/lib/server
.
حتى الآن يمكننا تنفيذ هذا الأمر. لكن أولاً يجب أن أضيف ذلك لتشغيل
-XX:+PrintAssembly
تحتاج أيضًا إلى إضافة خيار
-XX:+UnlockDiagnosticVMOptions
، ويجب أن يتبع قبل خيار
PrintAssembly
. إذا لم يتم ذلك ،
PrintAssembly
لك JVM تحذيرًا بشأن الاستخدام غير
PrintAssembly
لخيار
PrintAssembly
. لنقم بتشغيل هذا الكود:
java -client -Xbatch -XX:+PrintCompilation -XX:CompileCommandFile=hotspot_compiler -XX:CompileThreshold=5 -XX:-TieredCompilation -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly Test
سيكون الإخراج طويلًا ، وسيكون هناك خطوط مثل:
0x00007f4b7cab1120: mov 0x8(%rsi),%r10d 0x00007f4b7cab1124: shl $0x3,%r10 0x00007f4b7cab1128: cmp %r10,%rax
كما ترون ، يتم تجميع الوظائف المقابلة في رمز الجهاز الأصلي.
أخيرًا ، ناقش الخيار 3 ، AOT. التجميع قبل التنفيذ ، AOT ، لم يكن متاحًا في Java قبل الإصدار 9.
ظهرت أداة جديدة في JDK 9 ، jaotc - كما يوحي الاسم ، فهي مترجم AOT لجافا. الفكرة هي: تشغيل برنامج Java "javac" ، ثم برنامج التحويل البرمجي AOT لـ Java "jaotc" ، ثم تشغيل JVM "java" كالمعتاد. ينفذ JVM عادةً ترجمة وتصنيف JIT. ومع ذلك ، إذا كانت الوظيفة تحتوي على شفرة مترجمة من AOT ، فإنها تستخدمها مباشرة ، ولا تلجأ إلى الترجمة الشفوية أو ترجمة JIT. اسمحوا لي أن أشرح: ليس لديك لتشغيل برنامج التحويل البرمجي AOT ، إنه اختياري ، وإذا كنت تستخدمه ، يمكنك فقط ترجمة الفئات التي تريدها قبل تنفيذها.
دعنا نبني مكتبة تتكون من نسخة مترجمة من AOT من
Test::f
. لا تنسى: أن تفعل ذلك بنفسك ، ستحتاج إلى JDK 9 في الإصدار 150+.
jaotc --output=libTest.so Test.class
نتيجة لذلك ،
libTest.so
إنشاء
libTest.so
، وهي مكتبة تحتوي على رمز وظائف أصلية مترجمة من
libTest.so
مدرج في فئة
Test
. يمكنك عرض الحروف المحددة في هذه المكتبة:
nm libTest.so
في الختام ، من بين أمور أخرى ، سيكون هناك:
0000000000002120 t Test.f()I 00000000000021a0 t Test.<init>()V 00000000000020a0 t Test.main([Ljava/lang/String;)V
لذلك ، جميع وظائفنا ، المنشئ ،
f
، الطريقة الثابتة
main
موجودة في مكتبة
libTest.so
.
كما هو الحال بالنسبة لخيار "java" المقابل ، في هذه الحالة يمكن أن يكون الخيار مصحوبًا بملف ، لذلك يوجد خيار -compile-command to jaotc. تقدم JEP 295 الأمثلة ذات الصلة التي لن أعرضها هنا.
لنقم الآن بتشغيل "java" ومعرفة ما إذا كانت أساليب AOT المترجمة تستخدم. إذا قمت بتشغيل "java" كما كان من قبل ، فلن يتم استخدام مكتبة AOT ، وهذا ليس مفاجئًا. لاستخدام هذه الميزة الجديدة ، يتم توفير الخيار
-XX:AOTLibrary
، والذي يجب عليك تحديده:
java -XX:AOTLibrary=./libTest.so Test
يمكنك تحديد مكتبات AOT متعددة ، مفصولة بفواصل.
ناتج هذا الأمر هو نفسه تمامًا عند بدء تشغيل "java" بدون
AOTLibrary
، نظرًا لأن سلوك برنامج Test لم يتغير على الإطلاق. للتحقق من استخدام الوظائف المترجمة من
-XX:+PrintAOT
، يمكنك إضافة خيار جديد آخر ،
-XX:+PrintAOT
.
java -XX:AOTLibrary=./libTest.so -XX:+PrintAOT Test
قبل إخراج برنامج
Test
، يعرض هذا الأمر ما يلي:
9 1 loaded ./libTest.so aot library 99 1 aot[ 1] Test.main([Ljava/lang/String;)V 99 2 aot[ 1] Test.f()I 99 3 aot[ 1] Test.<init>()V
كما هو مخطط ، يتم تحميل مكتبة AOT ، ويتم استخدام وظائف المترجمة من AOT.
إذا كنت مهتمًا ، يمكنك تشغيل الأمر التالي وتحقق مما إذا كان تجميع JIT يحدث.
java -client -Xbatch -XX:+PrintCompilation -XX:CompileCommandFile=hotspot_compiler -XX:CompileThreshold=5 -XX:-TieredCompilation -XX:AOTLibrary=./libTest.so -XX:+PrintAOT Test
كما هو متوقع ، لا يتم إجراء ترجمة JIT ، حيث يتم تجميع الأساليب الموجودة في فئة الاختبار قبل التنفيذ ويتم توفيرها كمكتبة.
والسؤال المحتمل هو: إذا قمنا بتوفير رمز دالة أصلي ، فكيف تحدد JVM ما إذا كانت الشفرة الأصلية قديمة / لا معنى لها؟ كمثال أخير ، دعونا نقوم بتعديل الدالة
f
وتعيين 6.
public int f() throws Exception { int a = 6; return a; }
فعلت هذا فقط لتعديل ملف الفصل. الآن نجعل javac يترجم ونشغل نفس الأمر على النحو الوارد أعلاه.
javac Test.java java -client -Xbatch -XX:+PrintCompilation -XX:CompileCommandFile=hotspot_compiler -XX:CompileThreshold=5 -XX:-TieredCompilation -XX:AOTLibrary=./libTest.so -XX:+PrintAOT Test
كما ترون ، لم أقم بتشغيل "jaotc" بعد "javac" ، وبالتالي فإن الكود من مكتبة AOT أصبح قديمًا وغير صحيح الآن ، والوظيفة
f
لها = 5.
يوضح إخراج الأمر "java" أعلاه:
228 56 b Test::<init> (5 bytes) 229 57 b Test::f (5 bytes)
هذا يعني أن الوظائف في هذه الحالة تم تجميعها ديناميكيًا ، لذلك لم يتم استخدام الكود الناتج من التحويل البرمجي AOT. لذلك ، تم اكتشاف تغيير في ملف الفصل الدراسي. عند إجراء التحويل البرمجي باستخدام javac ، يتم إدخال بصمة الأصابع في الفصل ، ويتم أيضًا تخزين بصمة الأصابع في مكتبة AOT. نظرًا لأن بصمة الفصل الجديدة تختلف عن تلك المخزنة في مكتبة AOT ، لم يتم استخدام الكود الأصلي الذي تم تجميعه مسبقًا (AOT). هذا كل ما أردت إخبارك به عن خيار التحويل البرمجي الأخير ، قبل التنفيذ.
في هذا المقال ، حاولت أن أشرح وأوضح بأمثلة واقعية بسيطة كيف ينفذ JVM شفرة Java: تفسيرها ، وتجميعها ديناميكيًا (JIT) أو مقدمًا (AOT) - علاوة على ذلك ، ظهرت الفرصة الأخيرة فقط في JDK 9. أتمنى أن تكون قد تعلمت شيئًا ما. الجديد.