في هذه المقالة ، سنتحدث عن تفاصيل التنفيذ وتشغيل العديد من برامج التحويل البرمجي JIT ، بالإضافة إلى استراتيجيات التحسين. سنناقش بالتفصيل الكافي ، لكننا سنحذف العديد من المفاهيم المهمة. وهذا يعني أنه لن تكون هناك معلومات كافية في هذه المقالة للوصول إلى استنتاجات معقولة في أي مقارنة بين التطبيقات واللغات.
للحصول على فهم أساسي لمترجمات JIT ، اقرأ هذه المقالة .
ملاحظة صغيرة:
, , , - . , JIT ( ), , . , , , , . - , , .
- Pypy
- GraalVM C
- OSR
- JIT
()
يستخدم LuaJIT ما يسمى بالتتبع. يقوم Pypy بتنفيذ metatracing ، أي أنه يستخدم النظام لإنشاء مترجمي التتبع و JIT. Pypy و LuaJIT ليسا نموذجين لتطبيقات Python و Lua ، لكنهما مشروعان مستقلان. أود أن أصف LuaJIT بأنه سريع للغاية ، ويصف نفسه بأنه أحد أسرع تطبيقات اللغة الديناميكية - وأنا أؤمن بذلك تمامًا.
لفهم وقت بدء التتبع ، تبحث حلقة المترجم الفوري عن الحلقات الساخنة (مفهوم الكود الساخن عالمي لجميع مترجمي JIT!). ثم "يتتبع" المترجم الحلقة ، ويسجل العمليات القابلة للتنفيذ لتجميع كود الآلة المحسن جيدًا. في LuaJIT ، يعتمد التجميع على آثار مع تمثيل وسيط شبيه بالتعليمات يكون فريدًا لـ LuaJIT.
كيف يتم تنفيذ التتبع في Pypy
يبدأ Pypy في تتبع الوظيفة بعد 1619 تنفيذًا ويقوم بالتجميع بعد 1039 تنفيذًا ، أي أنه يستغرق حوالي 3000 تنفيذ للوظيفة لبدء التشغيل بشكل أسرع. تم اختيار هذه القيم بعناية من قبل فريق Pypy ، وبشكل عام في عالم المجمعين ، يتم اختيار العديد من الثوابت بعناية.
تجعل اللغات الديناميكية التحسين أمرًا صعبًا. يمكن إزالة الشفرة أدناه بشكل ثابت بلغة أكثر صرامة لأنها
False
ستكون خاطئة دائمًا. ومع ذلك ، في Python 2 ، لا يمكن ضمان ذلك حتى وقت التشغيل.
if False:
print("FALSE")
بالنسبة لأي برنامج ذكي ، ستكون هذه الحالة خاطئة دائمًا. لسوء الحظ ،
False
يمكن إعادة تعريف القيمة ، وسيكون التعبير في حلقة ، ويمكن إعادة تعريفه في مكان آخر. لذلك يمكن لـ Pypy إنشاء "حارس". إذا فشل المدافع ، يعود JIT إلى حلقة الترجمة. يستخدم Pypy بعد ذلك ثابتًا آخر (200) يسمى تتبع الرغبة في تحديد ما إذا كان سيتم تجميع بقية المسار الجديد قبل نهاية الحلقة. يسمى هذا المسار الفرعي جسرًا .
بالإضافة إلى ذلك ، يوفر Pypy هذه الثوابت كوسيطات يمكنك تخصيصها في وقت التشغيل جنبًا إلى جنب مع التكوين غير القابل للامتداد ، أي توسيع الحلقة والتضمين! بالإضافة إلى ذلك ، فإنه يوفر خطافات يمكننا رؤيتها بعد اكتمال التجميع.
def print_compiler_info(i):
print(i.type)
pypyjit.set_compile_hook(print_compiler_info)
for i in range(10000):
if False:
pass
print(pypyjit.get_stats_snapshot().counters)
أعلاه ، كتبت برنامج Python خالصًا مع خطاف تجميع لعرض نوع التجميع المطبق. يقوم الكود أيضًا بإخراج البيانات في النهاية ، مما يوضح عدد المدافعين. بالنسبة لهذا البرنامج ، حصلت على تجميع حلقة واحدة و 66 مدافعًا. عندما استبدلت التعبير
if
بتمريرة بسيطة خارج الحلقة for
، لم يتبق سوى 59 مدافعًا.
for i in range(10000):
pass # removing the `if False` saved 7 guards!
بإضافة هذين السطرين إلى الحلقة
for
، حصلت على مجموعتين ، أحدهما من النوع "الجسر"!
if random.randint(1, 100) < 20:
False = True
انتظر ، كنت تتحدث عن تتبع ميتا!
يمكن وصف فكرة metatracing بأنها "اكتب مترجمًا فوريًا واحصل على مترجم مجانًا!" أو "حوّل المترجم الفوري إلى مترجم JIT!" كتابة مترجم أمر صعب ، وإذا كان بإمكانك الحصول عليه مجانًا ، فإن الفكرة رائعة. Pypy "يحتوي" على مترجم ومترجم ، لكنه لا يطبق بشكل صريح المترجم التقليدي.
لدى Pypy أداة RPython (مصممة لـ Pypy). إنه إطار عمل لكتابة المترجمين الفوريين. لغتها نوع من Python وهي مكتوبة بشكل ثابت. في هذه اللغة ، يجب أن تكتب مترجمًا. لم يتم تصميم اللغة لبرمجة Python المكتوبة لأنها لا تحتوي على مكتبات أو حزم قياسية. أي برنامج RPython هو برنامج Python صالح. يتم تحويل كود RPython إلى C ثم تجميعه. وبالتالي ، يوجد مترجم meta في هذه اللغة كبرنامج C مترجم.
تعني البادئة "meta" في كلمة metatraces أن التتبع يتم عند تشغيل المترجم وليس البرنامج. يتصرف بشكل أو بآخر مثل أي مترجم آخر ، ولكن يمكنه تتبع عملياته وهو مصمم لتحسين عمليات التتبع من خلال تحديث مساره. مع مزيد من التتبع ، يصبح مسار المترجم الفوري أكثر تحسينًا. المترجم المحسن يتبع مسارًا محددًا. ويمكن استخدام رمز الآلة المستخدم في هذا المسار ، الذي تم الحصول عليه من خلال تجميع RPython ، في التجميع النهائي.
باختصار ، يقوم "المترجم" في Pypy بترجمة مترجمك ، وهذا هو سبب تسمية Pypy أحيانًا بالمترجم الفوقي. فهو لا يجمع الكثير من البرنامج الذي تقوم بتنفيذه باعتباره مسار مترجم محسن.
قد يبدو مفهوم Metatracing محيرًا ، لذلك ، لأغراض التوضيح ، كتبت برنامجًا سيئًا للغاية لا يفهم إلا
a = 0
و a++to
.
# interpreter written with RPython
for line in code:
if line == "a = 0":
alloc(a, 0)
elif line == "a++":
guard(a, "is_int") # notice how in Python, the type is unknown, but after being interpreted by RPython, the type is known
guard(a, "> 0")
int_add(a, 1)
إذا قمت بتشغيل هذه الدورة الساخنة:
a = 0
a++
a++
قد تبدو المسارات كما يلي:
# Trace from numerous logs of the hot loop
a = alloc(0) # guards can go away
a = int_add(a, 1)
a = int_add(a, 2)
# optimize trace to be compiled
a = alloc(2) # the section of code that executes this trace _is_ the compiled code
لكن المترجم ليس وحدة منفصلة خاصة ، فهو مدمج في المترجم الفوري. لذلك ، ستبدو دورة التفسير كما يلي:
for line in code:
if traces.is_compiled(line):
run_compiled(traces.compiled(line))
continue
elif traces.is_optimized(line):
compile(traces.optimized(line))
continue
elif line == "a = 0"
# ....
مقدمة في JVM
قضيت أربعة أشهر في الكتابة بلغة TruffleRuby في Graal ووقعت في حبها.
Hotspot (سميت بهذا الاسم لأنها تبحث عن النقاط الفعالة ) عبارة عن جهاز افتراضي يأتي مع تثبيتات Java قياسية. يحتوي على العديد من المجمعين لتنفيذ التجميع متعدد المستويات. قاعدة بيانات خط Hotspot البالغ عددها 250.000 سطر مفتوحة ولديها ثلاثة جامعي للقمامة. يتواءم Dev مع تجميع JIT ، في بعض المعايير ، يعمل بشكل أفضل من الضمانات C ++ (في هذه المناسبة مثل
لقد ألهمت الاستراتيجيات المستخدمة في Hotspot العديد من مؤلفي برامج التحويل البرمجي JIT اللاحقة ، وأطر الآلة الافتراضية للغة ، وخاصة محركات Javascript. أنتجت Hotspot أيضًا موجة من لغات JVM مثل Scala و Kotlin و JRuby و Jython. JRuby و Jython هي تطبيقات Ruby و Python الممتعة التي تجمع التعليمات البرمجية المصدر إلى JVM bytecode ، والتي تقوم Hotspot بتنفيذها بعد ذلك. كل هذه المشاريع ناجحة نسبيًا في تسريع Python و Ruby (Ruby أكثر من Python) دون تنفيذ كل الأدوات ، كما هو الحال مع Pypy. تعتبر Hotspot فريدة أيضًا من حيث أنها JIT للغات الأقل ديناميكية (على الرغم من أنها تقنيًا JIT لـ JVM bytecode ، وليس Java).
GraalVM هو JavaVM به جزء من كود Java. يمكنه تنفيذ أي لغة JVM (Java ، Scala ، Kotlin ، إلخ). كما أنه يدعم الصورة الأصلية للعمل مع كود AOT المترجم من خلال Substrate VM. تعمل نسبة كبيرة من خدمات سكالا على تويتر على Graal ، والتي تتحدث عن جودة الآلة الافتراضية ، وهي في بعض النواحي أفضل من JVM ، على الرغم من أنها مكتوبة بلغة Java.
وهذا ليس كل شيء! يوفر GraalVM أيضًا Truffle: إطار عمل لتطبيق اللغات عن طريق إنشاء مترجمين AST (شجرة بناء الجملة المجردة). لا توجد خطوة واضحة في Truffle عندما يتم إنشاء كود JVM bytecode كما هو الحال في لغة JVM العادية. بدلاً من ذلك ، سيستخدم Truffle ببساطة المترجم الفوري ويتحدث إلى Graal لإنشاء رمز الآلة مباشرةً مع التنميط وما يسمى بالتسجيل الجزئي. التقييم الجزئي خارج نطاق هذه المقالة ، باختصار: تلتزم هذه الطريقة بفلسفة "اكتب مترجمًا ، احصل على مترجم مجانًا!"
TruffleJS — Truffle- Javascript, V8 , , V8 , Google , . TruffleJS «» V8 ( JS-) , Graal.
JIT-
C
غالبًا ما تواجه تطبيقات JIT مشكلة في دعم امتدادات C. يمتلك المترجمون القياسيون مثل Lua و Python و Ruby و PHP واجهة برمجة تطبيقات لـ C تتيح للمستخدمين إنشاء حزم بهذه اللغة ، مما يؤدي إلى تسريع التنفيذ بشكل كبير. تتم كتابة العديد من الحزم بلغة C ، على سبيل المثال numpy ووظائف المكتبة القياسية مثل
rand
. كل امتدادات C هذه ضرورية لجعل اللغات المفسرة سريعة.
يصعب الحفاظ على امتدادات C لعدد من الأسباب. السبب الأكثر وضوحًا هو أن واجهة برمجة التطبيقات مصممة مع وضع التنفيذ الداخلي في الاعتبار. علاوة على ذلك ، من الأسهل دعم امتدادات C عند كتابة المترجم الفوري بلغة C ، لذلك لا يمكن لـ JRuby دعم امتدادات C ، ولكن لديها واجهة برمجة تطبيقات لملحقات Java. أصدر Pypy مؤخرًا إصدارًا تجريبيًا لدعم امتدادات C ، على الرغم من أنني لست متأكدًا مما إذا كان يعمل بسبب قانون Hyrum . يدعم LuaJIT امتدادات C ، بما في ذلك الميزات الإضافية في امتدادات C (LuaJIT رائع!)
يحل Graal هذه المشكلة مع Sulong ، وهو محرك ينفذ LLVM bytecode على GraalVM ، ويحوله إلى Truffle. LLVM عبارة عن صندوق أدوات ، ونحتاج فقط إلى معرفة أنه يمكن تجميع C إلى LLVM bytecode (تمتلك Julia واجهة LLVM أيضًا!). إنه أمر غريب ، لكن الحل هو أن نأخذ لغة مترجمة جيدة لها أكثر من أربعين عامًا من التاريخ وتفسيرها! بالطبع ، لا يعمل بسرعة مثل لغة C المترجمة بشكل صحيح ، ولكنه يحصل على العديد من الفوائد.
LLVM bytecode هو بالفعل منخفض المستوى تمامًا ، أي أنه ليس من غير الفعال تطبيق JIT على هذا التمثيل الوسيط كما هو الحال في C. يتم تعويض جزء من التكلفة من خلال حقيقة أنه يمكن تحسين الرمز الثنائي مع بقية برنامج Ruby ، ولكن لا يمكننا تحسين برنامج C المترجم ... يمكن تطبيق كل شرائح الذاكرة هذه ، والمضمنة ، والقطع الميتة ، والمزيد على كود C و Ruby ، بدلاً من استدعاء ثنائي C من كود Ruby. امتدادات TruffleRuby C أسرع من امتدادات CRuby C في بعض النواحي.
لكي يعمل هذا النظام ، عليك أن تعرف أن Truffle مستقلة تمامًا عن اللغة وأن عبء التبديل بين C أو Java أو أي لغة أخرى داخل Graal سيكون ضئيلًا.
تعد قدرة Graal على العمل مع Sulong جزءًا من قدراتها متعددة اللغات ، والتي تتيح إمكانية التبادل اللغوي العالي. هذا ليس مفيدًا فقط للمترجم ، ولكنه يثبت أيضًا أنه يمكنك بسهولة استخدام لغات متعددة في "تطبيق" واحد.
بالعودة إلى الشفرة المفسرة ، إنها أسرع
نحن نعلم أن JITs تحتوي على مترجم ومترجم ، وأنها تنتقل من مترجم إلى مترجم لتسريع الأمور. ينشئ Pypy جسورًا لمسار العودة ، على الرغم من أنه من وجهة نظر Graal و Hotspot ، فإن هذا يعد إلغاء تحسين . نحن لا نتحدث عن مفاهيم مختلفة تمامًا ، ولكن من خلال إلغاء التحسين نعني العودة إلى المترجم كتحسين واعٍ ، وليس حلاً لحتمية اللغة الديناميكية. يستخدم Hotspot و Graal بشكل نشط إلغاء التحسين ، خاصةً Graal ، لأن المطورين لديهم سيطرة صارمة على التجميع ، ويحتاجون إلى مزيد من التحكم في التجميع من أجل التحسينات (مقارنة بـ Pypy مثلاً). يتم استخدام Deoptimization أيضًا في محركات JS ، والتي سأتحدث عنها كثيرًا ، نظرًا لأن JavaScript في Chrome و Node.js يعتمد عليها.
لتطبيق إلغاء التحسين بسرعة ، من المهم التأكد من التبديل بين المترجم والمترجم الفوري بأسرع ما يمكن. مع التطبيق الأكثر سذاجة ، سيتعين على المترجم الفوري "اللحاق" بالمترجم من أجل أداء إلغاء التحسين. ترتبط المضاعفات الإضافية بإلغاء تحسين التدفقات غير المتزامنة. ينشئ Graal مجموعة من الإطارات ويطابقها مع الكود الذي تم إنشاؤه للعودة إلى المترجم. باستخدام النقاط الآمنة ، يمكنك إيقاف مؤشر ترابط Java مؤقتًا والقول ، "مرحبًا ، جامع القمامة ، هل أحتاج إلى التوقف؟" حتى لا تتطلب معالجة الخيط الكثير من النفقات العامة. اتضح أنه كان خامًا إلى حد ما ، لكنه يعمل بسرعة كافية لإلغاء التحسين ليكون إستراتيجية جيدة.
على غرار مثال جسر Pypy ، يمكن أيضًا إلغاء تحسين الترقيع القرد للوظائف. إنه أكثر أناقة لأننا نضيف كود إلغاء التحسين ليس عندما يفشل المدافع ، ولكن عند تطبيق ترقيع حرب العصابات.
مثال رائع على إلغاء تحسين JIT: فائض التحويل مصطلح غير رسمي. نحن نتحدث عن موقف
int32
يتم فيه تمثيل / تخصيص نوع معين (على سبيل المثال ، ) داخليًا ، ولكن يجب التحويل إليه int64
. يقوم TruffleRuby بهذا من خلال إلغاء التحسينات ، تمامًا مثل V8.
على سبيل المثال ، إذا سألت في Ruby
var = 0
، فستحصل على int32
(تسميها Ruby Fixnum
و Bignum
، لكنني سأستخدم الترميز int32
و int64
). إجراء عملية باستخدامvar
، تحتاج إلى التحقق من حدوث تجاوز للقيمة. ولكن هذا شيء يجب التحقق منه ، وتجميع الكود الذي يتعامل مع الفائض يعد مكلفًا ، خاصة بالنظر إلى تكرار العمليات الرقمية.
دون النظر إلى التعليمات المجمعة ، يمكنك أن ترى كيف يقلل هذا التحسين من كمية الكود.
int a, b;
int sum = a + b;
if (overflowed) {
long bigSum = a + b;
return bigSum;
} else {
return sum;
}
int a, b;
int sum = a + b;
if (overflowed) {
Deoptimize!
}
في TruffleRuby ، يتم إلغاء تحسين التشغيل الأول فقط لعملية معينة ، لذلك لا نهدر الموارد في كل مرة يتم فيها تجاوز العملية.
كود WET هو كود سريع. مضمنة و OSR
function foo(a, b) {
return a + b;
}
for (var i = 0; i < 1000000; i++) {
foo(i, i + 1);
}
foo(1, 2);
حتى التفاصيل الصغيرة مثل هذه المشغلات يتم إلغاء تحسينها في V8! باستخدام خيارات مثل
--trace-deopt
و ، --trace-opt
يمكنك جمع الكثير من المعلومات حول JIT وتعديل السلوك أيضًا. يحتوي Graal على بعض الأدوات المفيدة للغاية ، لكنني سأستخدم V8 لأن العديد من المستخدمين قاموا بتثبيته.
يبدأ Deoptimization بالسطر الأخير (
foo(1, 2)
) ، وهو أمر محير ، لأن هذه المكالمة تم إجراؤها في الحلقة! سوف نتلقى رسالة "تعليقات نوع غير كافية للاتصال" (القائمة الكاملة لأسباب إلغاء التحسين موجودة هنا ، وهناك سبب مضحك لـ "عدم وجود سبب" فيها). يؤدي هذا إلى إنشاء إطار إدخال يعرض القيم الحرفية 1
و 2
.
فلماذا إلغاء التحسين؟ V8 ذكي بما يكفي للقيام بعمليات التلبيس: عندما
i
يكون من النوعinteger
، يتم تمرير القيم الحرفية أيضًا integer
.
لفهم هذا ، دعنا نستبدل السطر الأخير بـ
foo(i, i +1)
. ولكن لا يزال إلغاء التحسين مطبقًا ، ولكن هذه المرة فقط تختلف الرسالة: "نوع التغذية المرتدة غير كافية للعملية الثنائية". لماذا ا؟! بعد كل شيء ، هذه بالضبط نفس العملية التي يتم إجراؤها في حلقة ، مع نفس المتغيرات!
الجواب يا صديقي يكمن في الاستبدال المتراكم (OSR). التضمين هو تحسين مترجم قوي (ليس فقط JIT) حيث تتوقف الوظائف عن أن تكون وظائف ، ويتم تمرير المحتوى إلى مكان الاستدعاءات. يمكن لمجمعي JIT المضمنة لزيادة السرعة عن طريق تغيير الكود في وقت التشغيل (يمكن للغات المترجمة أن تكون مضمنة بشكل ثابت فقط).
// partial output from printing inlining details
[compiling method 0x04a0439f3751 <JSFunction (sfi = 0x4a06ab56121)> using TurboFan OSR]
0x04a06ab561e9 <SharedFunctionInfo foo>: IsInlineable? true
Inlining small function(s) at call site #49:JSCall
لذلك فإن V8 سيترجم
foo
ويحدد أنه يمكن أن يكون مضمنًا ومتماشيًا مع OSR. ومع ذلك ، فإن المحرك يفعل ذلك فقط من أجل الكود داخل الحلقة ، لأن هذا مسار ساخن ، والسطر الأخير لم يكن بعد في المترجم وقت التضمين. لذلك ، لا يحتوي V8 حتى الآن على ملاحظات كافية حول نوع الوظيفة foo
، لأنه لا يتم استخدامه في الحلقة ، ولكن إصدارها المضمّن. إذا تم تطبيقه --no-use-osr
، فلن يكون هناك deoptimization ، بغض النظر عما نجتازه ، حرفيًا أو i
. ومع ذلك ، بدون تضمين ، ستعمل حتى مليون عملية تكرار بشكل أبطأ بشكل ملحوظ. المترجمون JIT يجسدون حقًا مبدأ عدم وجود حل فقط للمقايضة. تعد عمليات إلغاء التهيئة باهظة الثمن ، لكنها لا تقارن بتكلفة البحث عن الأساليب والتضمين ، وهو ما يُفضل في هذه الحالة.
التضمين فعال بشكل لا يصدق! قمت بتشغيل الكود أعلاه مع اثنين من الأصفار الإضافية ، ومع إيقاف تشغيل البطانة ، تم تشغيله أبطأ أربع مرات.
على الرغم من أن هذه المقالة تدور حول JIT ، إلا أن التضمين فعال في اللغات المجمعة أيضًا. تستخدم جميع لغات LLVM التضمين بنشاط ، لأن LLVM ستقوم بذلك أيضًا ، على الرغم من أن Julia مضمنة بدون LLVM ، هذا في طبيعتها. يمكن تضمين JITs باستخدام أساليب الاستدلال في وقت التشغيل وتكون قادرة على التبديل بين الأوضاع غير المضمنة والمضمنة باستخدام OSR.
ملاحظة على JIT و LLVM
يوفر LLVM مجموعة من الأدوات المتعلقة بالتجميع. تعمل Julia مع LLVM (لاحظ أن هذا صندوق أدوات كبير وكل لغة تستخدمه بشكل مختلف) ، تمامًا مثل Rust و Swift و Crystal. يكفي أن نقول إن هذا مشروع كبير ورائع يدعم أيضًا JITs ، على الرغم من أن LLVM لا تحتوي على JITs ديناميكية مدمجة. استخدم المستوى الرابع من تجميع JavaScriptCore الواجهة الخلفية LLVM لفترة من الوقت ، ولكن تم استبدالها منذ أقل من عامين. منذ ذلك الحين ، لم تكن مجموعة الأدوات هذه مناسبة تمامًا لأجهزة JIT الديناميكية ، ويرجع ذلك أساسًا إلى أنها غير مصممة للعمل في بيئة ديناميكية. حاول Pypy 5-6 مرات ، لكنه استقر على JSC. مع LLVM ، كان غرق التخصيص وحركة الكود محدودًا.كان من المستحيل أيضًا استخدام ميزات JIT القوية مثل استنتاج النطاق (يشبه الإرسال ، ولكن مع نطاق معروف من القيم). ولكن الأهم من ذلك ، مع LLVM ، يتم إنفاق الكثير من الموارد على التجميع.
ماذا لو ، بدلاً من التمثيل الوسيط القائم على التعليمات ، لدينا رسم بياني كبير يعدل نفسه؟
تحدثنا عن LLVM bytecode و Python / Ruby / Java bytecode كتمثيل متوسط. كلهم يبدون وكأنهم نوع من اللغة في شكل تعليمات. تستخدم Hotspot و Graal و V8 التمثيل الوسيط "Sea of Nodes" (المقدم في Hotspot) ، وهو مستوى AST منخفض. هذه طريقة عرض فعالة لأن جزءًا مهمًا من التنميط يعتمد على فكرة مسار معين نادرًا ما يتم استخدامه (أو يتم تجاوزه في حالة وجود نمط ما). لاحظ أن برامج التحويل البرمجي لـ AST تختلف عن المحللات اللغوية لـ AST.
عادةً ما ألتزم بالموقف "حاول القيام بذلك في المنزل!" ، ولكن من الصعب جدًا التفكير في الرسوم البيانية ، رغم أنها مثيرة جدًا للاهتمام ، وغالبًا ما تكون مفيدة للغاية لفهم عمل المترجم. على سبيل المثال ، لا يمكنني قراءة جميع الرسوم البيانية ليس فقط بسبب نقص المعرفة ، ولكن أيضًا بسبب القدرات الحسابية لعقلي (يمكن أن تساعد خيارات المترجم في التخلص من السلوك الذي لست مهتمًا به).
في حالة V8 ، سنستخدم أداة D8 مع العلم
--print-ast
. بالنسبة لغراال سيكون كذلك --vm.Dgraal.Dump=Truffle:2
. سيتم عرض النص على الشاشة (منسق للحصول على رسم بياني). لا أعرف كيف يقوم مطورو V8 بإنشاء الرسوم البيانية المرئية ، لكن Oracle لديها "Ideal Graph Visualizer" والذي يتم استخدامه في الرسم التوضيحي السابق. لم يكن لدي القوة لإعادة تثبيت IGV ، لذلك أخذت الرسوم البيانية من Chris Seaton ، التي تم إنشاؤها باستخدام Seafoam ، الذي تم إغلاق مصدره الآن.
حسنًا ، دعنا نلقي نظرة على JavaScript AST!
function accumulate(n, a) {
var x = 0;
for (var i = 0; i < n; i++) {
x += a;
}
return x;
}
accumulate(1, 1)
قمت بتشغيل هذا الكود من خلاله
d8 --print-ast test.js
، على الرغم من أننا مهتمون فقط بالوظيفة accumulate
. لاحظ أنني اتصلت به مرة واحدة فقط ، أي لست مضطرًا إلى انتظار التجميع للحصول على AST.
هذا ما يبدو عليه AST (أزلت بعض الأسطر غير المهمة):
FUNC at 19
. NAME "accumulate"
. PARAMS
. . VAR (0x7ff5358156f0) (mode = VAR, assigned = false) "n"
. . VAR (0x7ff535815798) (mode = VAR, assigned = false) "a"
. DECLS
. . VARIABLE (0x7ff5358156f0) (mode = VAR, assigned = false) "n"
. . VARIABLE (0x7ff535815798) (mode = VAR, assigned = false) "a"
. . VARIABLE (0x7ff535815840) (mode = VAR, assigned = true) "x"
. . VARIABLE (0x7ff535815930) (mode = VAR, assigned = true) "i"
. BLOCK NOCOMPLETIONS at -1
. . EXPRESSION STATEMENT at 38
. . . INIT at 38
. . . . VAR PROXY local[0] (0x7ff535815840) (mode = VAR, assigned = true) "x"
. . . . LITERAL 0
. FOR at 43
. . INIT at -1
. . . BLOCK NOCOMPLETIONS at -1
. . . . EXPRESSION STATEMENT at 56
. . . . . INIT at 56
. . . . . . VAR PROXY local[1] (0x7ff535815930) (mode = VAR, assigned = true) "i"
. . . . . . LITERAL 0
. . COND at 61
. . . LT at 61
. . . . VAR PROXY local[1] (0x7ff535815930) (mode = VAR, assigned = true) "i"
. . . . VAR PROXY parameter[0] (0x7ff5358156f0) (mode = VAR, assigned = false) "n"
. . BODY at -1
. . . BLOCK at -1
. . . . EXPRESSION STATEMENT at 77
. . . . . ASSIGN_ADD at 79
. . . . . . VAR PROXY local[0] (0x7ff535815840) (mode = VAR, assigned = true) "x"
. . . . . . VAR PROXY parameter[1] (0x7ff535815798) (mode = VAR, assigned = false) "a"
. . NEXT at 67
. . . EXPRESSION STATEMENT at 67
. . . . POST INC at 67
. . . . . VAR PROXY local[1] (0x7ff535815930) (mode = VAR, assigned = true) "i"
. RETURN at 91
. . VAR PROXY local[0] (0x7ff535815840) (mode = VAR, assigned = true) "x"
من الصعب تحليل هذا ، لكنه مشابه لـ AST الخاص بالمحلل اللغوي (ليس صحيحًا لجميع البرامج). ويتم إنشاء AST التالي باستخدام Acorn.js.
والفرق الملحوظ هو تعريف المتغيرات. في AST من المحلل اللغوي ، لا يوجد تعريف واضح للمعلمات ، ويتم إخفاء إعلان الحلقة في العقدة
ForStatement
. في AST على مستوى المترجم ، يتم تجميع جميع الإعلانات مع العناوين والبيانات الوصفية.
يستخدم المترجم AST أيضًا هذا التعبير الغبي
VAR PROXY
. لا يمكن لـ AST للمحلل تحديد العلاقة بين الأسماء والمتغيرات (حسب العناوين) بسبب رفع المتغيرات (الرفع) والتقييم (التقييم) وغيرها. لذلك يستخدم المترجم AST المتغيرات PROXY
التي ترتبط لاحقًا بالمتغير الفعلي.
// This chunk is the declarations and the assignment of `x = 0`
. DECLS
. . VARIABLE (0x7ff5358156f0) (mode = VAR, assigned = false) "n"
. . VARIABLE (0x7ff535815798) (mode = VAR, assigned = false) "a"
. . VARIABLE (0x7ff535815840) (mode = VAR, assigned = true) "x"
. . VARIABLE (0x7ff535815930) (mode = VAR, assigned = true) "i"
. BLOCK NOCOMPLETIONS at -1
. . EXPRESSION STATEMENT at 38
. . . INIT at 38
. . . . VAR PROXY local[0] (0x7ff535815840) (mode = VAR, assigned = true) "x"
. . . . LITERAL 0
وهذه هي الطريقة التي تبدو بها AST لنفس البرنامج ، التي تم الحصول عليها باستخدام Graal!
يبدو أبسط بكثير. يشير اللون الأحمر إلى تدفق التحكم ، ويشير اللون الأزرق إلى تدفق البيانات ، ويشير السهم إلى الاتجاهات. لاحظ أنه في حين أن هذا الرسم البياني أبسط من AST من V8 ، فإن هذا لا يعني أن Graal أفضل في تبسيط البرنامج. لقد تم إنشاؤه بناءً على Java ، وهو أقل ديناميكية بكثير. سيكون نفس الرسم البياني Graal الذي تم إنشاؤه من Ruby أقرب إلى الإصدار الأول.
من المضحك أن AST في Graal سيتغير اعتمادًا على تنفيذ الكود. يتم إنشاء هذا الرسم البياني مع تعطيل OSR وضمه ، عندما يتم استدعاء الوظيفة بشكل متكرر باستخدام معلمات عشوائية بحيث لا يتم تحسينها. وسوف يزودك التفريغ بمجموعة كاملة من الرسوم البيانية! يستخدم Graal AST متخصصًا لتحسين البرامج (يقوم V8 بإجراء تحسينات مماثلة ، ولكن ليس على مستوى AST). عند حفظ الرسوم البيانية في Graal ، تحصل على أكثر من عشرة مخططات بمستويات تحسين مختلفة. عند إعادة كتابة العقد ، فإنها تستبدل نفسها (تتخصص) بالعقد الأخرى.
الرسم البياني أعلاه هو مثال ممتاز للتخصص في لغة مكتوبة ديناميكيًا (الصورة مأخوذة من One VM إلى Rule Them All ، 2013). يرتبط سبب وجود هذه العملية ارتباطًا وثيقًا بكيفية عمل التقييم الجزئي - الأمر كله يتعلق بالتخصص.
Hooray JIT جمعت الكود! دعونا تجميع مرة أخرى! ومره اخرى!
ذكرت أعلاه عن "متعدد المستويات" ، فلنتحدث عنها. الفكرة بسيطة: إذا لم نكن مستعدين بعد لإنشاء تعليمات برمجية محسّنة بالكامل ، لكن التفسير لا يزال مكلفًا ، فيمكننا التجميع مسبقًا ثم التجميع النهائي عندما نكون مستعدين لإنشاء كود أكثر تحسينًا.
Hotspot عبارة عن JIT ذات طبقات مع مترجمين ، C1 و C2. يقوم C1 بعمل ترجمة سريعة وتشغيل الكود ، ثم يقوم بعمل تنميط كامل للحصول على الكود المترجم باستخدام C2. هذا يمكن أن يساعد في حل العديد من مشاكل الإحماء. الكود المترجم غير المحسن أسرع من التفسير على أي حال. أيضًا ، لا تجمع C1 و C2 كل التعليمات البرمجية. إذا كانت الوظيفة تبدو بسيطة بما فيه الكفاية ، مع وجود احتمال كبير لن تساعدنا C2 ولن تعمل (سنوفر أيضًا الوقت في التنميط!). إذا كانت C1 مشغولة بالتجميع ، فيمكن متابعة عملية التشكيل ، وسيتم مقاطعة عمل C1 ويبدأ التجميع باستخدام C2.
يحتوي JavaScript Core على مستويات أكثر! في الواقع ، هناك ثلاثة JITs . يقوم مترجم JSC ببعض التنميط الخفيف ، ثم ينتقل إلى Baseline JIT ، ثم DFG (مخطط تدفق البيانات) JIT ، وأخيراً FTL (أسرع من الضوء) JIT. مع وجود العديد من المستويات ، لم يعد معنى إلغاء التحسين مقصورًا على الانتقال من مترجم إلى مترجم ، ويمكن إجراء إلغاء التحسين بدءًا من DFG وانتهاءًا بـ Baseline JIT (ليس هذا هو الحال في حالة Hotspot C2-> C1). يتم تنفيذ جميع عمليات إلغاء التحسين والانتقالات إلى المستوى التالي باستخدام OSR (تجاوز المكدس).
يتصل Baseline JIT بعد حوالي 100 تنفيذ ، و DFG JIT بعد حوالي 1000 (مع بعض الاستثناءات). هذا يعني أن JIT يحصل على الكود المترجم أسرع بكثير من نفس Pypy (الذي يأخذ حوالي 3000 عملية إعدام). يسمح التصفيف لـ JIT بمحاولة ربط مدة تنفيذ التعليمات البرمجية بمدة تحسينها. هناك مجموعة من الحيل حول نوع التحسين (تضمين ، صب ، إلخ) لأداء كل مستوى ، وبالتالي فإن هذه الإستراتيجية هي الأمثل.
مصادر مفيدة
- كيف يعمل مترجم LuaJIT's Trace Compiler من Mike Pall
- تأثير التتبع التلوي على الأجهزة الافتراضية بواسطة Laurie Tratt
- تحليل الهروب من Pypy
- لماذا لا يكون المستخدمون أكثر سعادة مع VMs بواسطة Laurie Tratt
- حول محركات JS:
- حول deoptimization:
- Graal:
- :
- :