نقل JS إلى Elbrus

هذه قصة حول نقل جافا سكريبت إلى منصة Elbrus المحلية ، من صنع رجال من UniPro. تقدم المقالة تحليلاً مقارنًا موجزًا ​​للمنصات وتفاصيل العمليات والمزالق.



تستند المقالة إلى تقرير أعده دميتري ( dbezheckov ) Bezhetskov وفلاديمير ( volodyabo ) Anufrienko مع HolyJS 2018 Piter. تحت القطع ستجد نص الفيديو والنص من التقرير.


الجزء 1. Elbrus ، في الأصل من روسيا


أولاً ، سوف نفهم ما هو Elbrus. فيما يلي بعض الميزات الرئيسية لهذا النظام الأساسي مقارنة بـ x86.

معمارية VLIW


حل معماري مختلف تمامًا عن العمارة الفوقية ، وهو أكثر شيوعًا في السوق الآن. يسمح لك VLIW بالتعبير بدقة أكبر عن النوايا في التعليمات البرمجية بسبب التحكم الواضح في جميع الأجهزة المنطقية الحسابية المستقلة (ALUs) ، التي تمتلكها شركة Elbrus ، بالمناسبة ، 4. هذا لا يستبعد إمكانية توقف بعض وحدات ALU ، ولكنه مع ذلك يزيد من الأداء النظري بدورة ساعة واحدة المعالج.

تجميع الفريق


يتم دمج أوامر المعالج الجاهز في حزم (حزم). حزمة واحدة هي تعليمات كبيرة واحدة يتم تنفيذها لكل ساعة شرطية. لديها العديد من التعليمات الذرية التي يتم تنفيذها بشكل مستقل وفوري في عمارة Elbrus.



في الصورة على اليمين ، تشير المستطيلات الرمادية إلى الحزم التي تم الحصول عليها عن طريق معالجة كود JS على اليسار. إذا كان كل شيء واضحًا تقريبًا مع التعليمات ldd و fmuld و faddd و fsqrts ، فإن بيان الإرجاع في بداية الحزمة الأولى يثير الدهشة للأشخاص الذين ليسوا على دراية بمجمع Elbrus. يقوم هذا التوجيه بتحميل عنوان الإرجاع من وظيفة floatMath الحالية في سجل ctpr3 مقدمًا ، حتى يتمكن المعالج من تنزيل الإرشادات الضرورية. بعد ذلك ، في الحزمة الأخيرة ، قمنا بالفعل بالانتقال إلى العنوان الذي تم تحميله مسبقًا في ctpr3.

ومن الجدير بالذكر أيضًا أن Elbrus لديها الكثير من التسجيلات 192 + 32 + 32 مقابل 16 + 16 + 8 لـ x86.

المضاربة الصريحة مقابل الضمنية


يدعم Elbrus المضاربة الصريحة على مستوى الأمر. لذلك ، يمكننا استدعاء a.bar وتحميله من الذاكرة حتى قبل التحقق من أنه ليس فارغًا ، كما هو موضح في الرمز الموجود على اليمين. إذا اتضح أن القراءة المنطقية في النهاية غير صالحة ، فسيتم ببساطة تمييز القيمة في b على أنها غير صحيحة ولن يكون من الممكن الوصول إليها.



دعم التنفيذ الشرطي


يدعم Elbrus أيضًا التنفيذ الشرطي. ضع في اعتبارك هذا في المثال التالي.



كما نرى ، يتم أيضًا تقليل الرمز من المثال السابق حول المضاربة بسبب استخدام تحويل التعبير الشرطي إلى الاعتماد ، ليس عن طريق التحكم ، ولكن عن طريق البيانات. يدعم جهاز Elbrus التسجيلات الأصلية ، حيث يمكنك تخزين قيمتين حقيقيتين أو خاطئتين فقط. ميزتها الرئيسية هي أنه يمكنك وضع علامة على التعليمات بمثل هذا المسند ، واعتمادًا على قيمتها في وقت التنفيذ ، سيتم تنفيذ التعليمات أم لا. في هذا المثال ، تقوم تعليمات cmpeq بإجراء المقارنة وتضع نتيجتها المنطقية في المسند P1 ، والذي يتم استخدامه بعد ذلك كعلامة لتحميل القيمة من b إلى النتيجة. وبناءً على ذلك ، إذا كان المسند يساوي صحيحًا ، فستظل القيمة 0 في النتيجة.

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

ثلاث مجموعات من الأجهزة مقارنة بواحدة في Intel


اثنان منهم محميان من التعديل من قبل المبرمج. واحد - مكدس السلسلة - مسؤول عن تخزين العناوين للإرجاع من الوظائف ، والآخر - مكدس التسجيل - يحتوي على المعلمات التي يتم تمريرها من خلالها. المكدس المستخدم الثالث - يخزن متغيرات المستخدم والبيانات. في Intel ، يتم تخزين كل شيء على مكدس واحد ، مما يؤدي إلى ظهور نقاط ضعف ، حيث أن جميع عناوين الانتقالات والمعلمات موجودة في مكان واحد غير محمي بواسطة تعديلات من قبل المستخدم.

لا يوجد توقع الفرع الديناميكي


بدلاً من ذلك ، يتم استخدام مخطط يحتوي على تحويلات if- وتحويلات انتقالية حتى لا يتوقف خط أنابيب التنفيذ.

فلماذا نحتاج شبيبة على Elbrus؟


  1. استبدال الاستيراد.
  2. مقدمة Elbrus إلى سوق أجهزة الكمبيوتر المنزلية ، حيث Javascript مطلوب بالفعل لنفس المتصفح.
  3. Elbrus مطلوب بالفعل في الصناعة ، على سبيل المثال مع Node.js. لذلك ، تحتاج إلى نقل العقدة إلى هذه البنية.
  4. تطوير بنية Elbrus ، بالإضافة إلى المتخصصين في هذا المجال.

إذا لم يكن هناك مترجم ، يأتي مترجمان


تم استخدام الإصدار السابق من الإصدار 8 من Google كأساس. يعمل مثل هذا: يتم إنشاء شجرة بناء جملة مجردة من التعليمات البرمجية المصدر ، ثم اعتمادًا على ما إذا تم تنفيذ التعليمات البرمجية أم لا ، باستخدام أحد المجمعين (Crankshaft أو FullCodegen) ، على التوالي ، يتم إنشاء كود ثنائي محسن أو غير محسن. لا يوجد مترجم.



كيف يعمل FullCodegen؟


تتم ترجمة عقد شجرة بناء الجملة إلى رمز ثنائي ، وبعد ذلك يتم "لصق" كل شيء معًا. عقدة واحدة حوالي 300 سطر من التعليمات البرمجية في مجمّع ماكرو. هذا ، أولاً ، يعطي أفقًا واسعًا من التحسينات ، وثانيًا ، لا توجد انتقالات رمز ثانوي ، كما هو الحال في المترجم. الأمر بسيط ، ولكن في نفس الوقت توجد مشكلة - أثناء النقل ، سيكون عليك إعادة كتابة الكثير من التعليمات البرمجية في المجمّع الكلي.



ومع ذلك ، تم كل هذا ، وكانت النتيجة إصدار مترجم كامل لـ FullCodegen لـ Elbrus. تم تنفيذ كل شيء من خلال وقت تشغيل C ++ v8 ، ولم يقوموا بتحسين أي شيء ، وتم إعادة كتابة رمز المجمع ببساطة من x86 إلى بنية Elbrus.

برنامج Codegen 1.1


ونتيجة لذلك ، لم تكن النتيجة هي نفسها تمامًا كما هو متوقع ، وتقرر إصدار FullCodegen 1.1:

  • جعل وقت التشغيل أقل ، كتب على مجمّع ماكرو ؛
  • تمت إضافة تحويلات if يدويًا (في الشكل ، كمثال ، يتم التحقق من المتغير js بحثًا عن صواب أو خطأ) ؛



لاحظ أن التحقق من NaN ، غير محدد ، وقيم فارغة يتم في كل مرة ، دون استخدام if ، وهو ما سيكون مطلوبًا في بنية Intel.

  • لم تتم إعادة كتابة الرمز فقط باستخدام Intel ، ولكن تم تنفيذ المضاربة في بذرة وتنفيذ المسار السريع أيضًا من خلال MAsm (مجمع الماكرو).

تم إجراء الاختبارات في Google Octane. آلات الاختبار:

  • Elbrus: E2S 750 ميجاهرتز ، 24 جيجابايت
  • انتل: كور اي 7 3.4 جيجا هيرتز ، 16 جيجا

مزيد من النتائج:



في الرسم البياني هو نسبة النتائج ، أي كم مرة يكون Elbrus أسوأ من Intel. في اختبارين ، Crypto و zlib ، تكون النتائج أسوأ بشكل ملحوظ بسبب حقيقة أن Elbrus ليس لديها تعليمات الأجهزة حتى الآن للعمل مع التشفير. بشكل عام ، نظرًا للاختلاف في الترددات ، اتضح أنه جيد جدًا.

فيما يلي اختبار بالمقارنة مع مترجم js من فايرفوكس ، وهو جزء من توزيع Elbrus القياسي. المزيد أفضل.


الحكم - قام المترجم بعمل جيد مرة أخرى.

نتائج التنمية


  • اجتاز محرك JS الجديد اختبارات test262. وهذا يعطيها الحق في أن تسمى بيئة وقت تشغيل كاملة ECMAScript 262.
  • زادت الإنتاجية في المتوسط ​​خمس مرات مقارنة بالمحرك السابق - المترجم.
  • تم أيضًا نقل Node.js 6.10 كمثال على استخدام V8 ، لأنه لم يكن صعبًا.
  • ومع ذلك ، فإنه لا يزال أسوأ من Core i7 على FullCodegen سبع مرات.

لا شيء يبدو أنه ينذر


سيكون كل شيء على ما يرام ، ولكن أعلنت Google هنا أنها لم تعد تدعم FullCodegen و Crankshaft وسيتم حذفها. وبعد ذلك تلقى الفريق أمر تطوير لمتصفح فايرفوكس ، والمزيد في وقت لاحق.



الجزء 2. فايرفوكس وقرد العنكبوت


حول محرك متصفح Firefox - SpiderMonkey. في الشكل ، الاختلافات بين هذا المحرك و V8 الأحدث.



يمكن ملاحظة أن كل شيء في المرحلة الأولى يبدو وكأنه يتم تحليل شفرة المصدر في شجرة بناء جملة مجردة ، ثم إلى رمز بايت ، ثم تبدأ الاختلافات.

في SpiderMonkey ، يتم تفسير الرمز البايت بواسطة مترجم C ++ ، والذي يشبه في جوهره مفتاحًا كبيرًا ، يتم داخله قفزات البايت كود. علاوة على ذلك ، يدخل الكود المفسر في خط الأساس للمترجم الجديد. ثم ، في المرحلة النهائية ، يتم تضمين المترجم الأمثل أيون في الحالة. في محرك V8 ، تتم معالجة الرمز الثانوي بواسطة مترجم Ingnition ، ثم بواسطة مترجم TurboFan.

خط الأساس ، أختار لك!


بدأ النقل مع مترجم Baseline. إنها في الأساس آلة مكدسة. بمعنى ، هناك مجموعة معينة من الخلايا التي تأخذ منها المتغيرات ، وتتذكرها ، وتقوم ببعض الإجراءات معها ، وبعد ذلك تقوم بإرجاع كل من المتغيرات ونتائج الإجراءات إلى خلايا المكدس. أدناه في بعض الصور تظهر هذه الآلية خطوة بخطوة فيما يتعلق بالوظيفة البسيطة foo:









ما هو الاطار؟




في الصور أعلاه ، يمكنك رؤية كلمة الإطار. تقريبًا ، هذا سياق جافا سكريبت على الأجهزة ، أي مجموعة بيانات على المكدس تصف أيًا من وظائفك. في الصورة أدناه ، الوظيفة foo ، وعلى يمينها ، كيف تبدو على المكدس: الحجج ، وصف الوظيفة ، عنوان المرسل ، الإشارة إلى الإطار السابق ، لأنه تم استدعاء الوظيفة من مكان ما ومن أجل العودة بشكل صحيح إلى مكان المكالمة يجب تخزين هذه المعلومات في المكدس ، ثم تعمل المتغيرات المحلية نفسها وتعمل مع العمليات الحسابية.



وبالتالي ، مزايا خط الأساس :

  • يبدو وكأنه FullCodegen ، لذا كانت تجربته في النقل مفيدة ؛
  • ميناء المجمع ، الحصول على مترجم العمل ؛
  • من السهل تصحيح.
  • يمكن إعادة كتابة أي كعب.

ولكن هناك أيضًا عيوب :

  • رمز خطي ، حتى تقوم بتنفيذ رمز بايت واحد ، لن تتمكن من تنفيذ ما يلي ، وهو ليس جيدًا جدًا للهندسة ذات الحوسبة المتوازية ؛
  • نظرًا لأنه يعمل مع البايت كود ، فأنت لا تقوم بالتحسين حقًا.

بقي فقط لتنفيذ المجمع الكلي والحصول على مترجم جاهز. لم يبشر التصحيح بشكل جيد ، كان يكفي أن ننظر إلى المكدس على بنية x86 ، ثم على العنصر الذي تم الحصول عليه عند النقل للعثور على المشكلة.

نتيجة لذلك ، في الاختبارات مع المترجم الجديد ، تضاعفت الإنتاجية ثلاث مرات:



ومع ذلك ، لا يدعم Octane الاستثناءات. وتنفيذها مهم جدا.

عمل استثنائي


أولاً ، دعنا نرى كيف تعمل الاستثناءات على x86. أثناء تشغيل البرنامج ، تتم كتابة عناوين الإرجاع من الوظائف إلى المكدس. عند نقطة ما ، يحدث استثناء. ننتقل إلى معالج استثناء وقت التشغيل ، الذي يستخدم الإطارات التي تحدثنا عنها أعلاه. نجد مكان حدوث الاستثناء بالضبط ، وبعد ذلك نحتاج إلى إرجاع التكدس إلى الحالة المطلوبة ، ثم يتغير عنوان الإرجاع إلى العنوان حيث ستتم معالجة الاستثناء.

المشكلة هي أنه بسبب جهاز مكدس آخر على بنية Elbrus ، لن يعمل هذا. سيكون من الضروري أن تحسب من خلال مكالمات النظام مقدار ما تحتاجه للترجيع في مكدس السلسلة. بعد ذلك ، نجري مكالمة نظام للحصول على مكدس المكالمة. بعد ذلك ، في العنوان في مكدس السلسلة ، نقوم باستبدال العنوان الذي يقوم بالعودة.

فيما يلي توضيح لتسلسل هذه الخطوات.



ليست أسرع طريقة ، ولكن يتم التعامل مع الاستثناء. ولكن مع ذلك ، على Intel تبدو أبسط قليلاً:



مع Elbrus ، سيكون هناك المزيد من القفزات إلى المعالج:


لهذا السبب لا يجب أن تبني منطق البرنامج على الاستثناءات ، خاصة على Elbrus.

تحسينه!


لذلك ، يتم تنفيذ معالجة الاستثناء. الآن سنخبرك كيف جعلنا الأمر أسرع قليلاً:

  • إعادة كتابة ذاكرة التخزين المؤقت المضمنة ؛

  • عمل ترتيب يدوي (ثم تلقائي) للتأخير ؛
  • لقد قاموا بالاستعدادات للتحولات (أعلى في الشفرة): كلما تم إعداد الانتقال مبكرًا ، كان ذلك أفضل ؛
  • جامع القمامة المتزايد المعتمدة

سوف تسكن الفقرة الثانية بمزيد من التفصيل. لقد قمنا بالفعل بفحص مثال صغير للعمل مع الحزم ، وسوف ننتقل إليها.



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



نتائج التحسين BaseLine v1.0 مقابل Baseline v1.1. بالتأكيد ، أصبح المحرك أسرع.



كيف لا يستطيع المبرمجون صنع بندقية أيون؟


على موجة النجاح من تطبيق Baseline v1.1 ، تقرر نقل المترجم الأمثل Ion.



كيف يعمل المترجم الأمثل؟ يتم تفسير التعليمات البرمجية المصدر ، بدأ التجميع. في عملية تنفيذ البايت كود ، يقوم Ion بجمع البيانات عن الأنواع المستخدمة في البرنامج ، وتحليل "الوظائف الساخنة" - تلك التي يتم تنفيذها في كثير من الأحيان أكثر من غيرها. بعد ذلك ، يتم اتخاذ القرار بتجميعها بشكل أفضل ، لتحسينها. بعد ذلك ، يتم إنشاء تمثيل عالي المستوى للمترجم ، رسم بياني للتشغيل. تم تحسين الرسم البياني (opt 1 ، opt 2 ، opt ...) ، يتم إنشاء تمثيل منخفض المستوى ، يتكون من تعليمات الجهاز ، يتم حجز السجلات ، يتم إنشاء رمز ثنائي محسن بشكل مباشر.



هناك المزيد من السجلات في Elbrus والفرق نفسها كبيرة ، لذا نحتاج:

  • مخطط الفريق
  • مخصص تسجيل خاص ؛
  • الخاصة LIR (تمثيل متوسط ​​المستوى) ؛
  • مولد الشفرة الخاص.

كان لدى الفريق بالفعل خبرة في نقل Java إلى Elbrus ، قرروا استخدام المكتبة نفسها لإنشاء التعليمات البرمجية لنقل Ion. تسمى TANGO. لديها:

  • مخطط الفريق
  • مخصص تسجيل خاص ؛
  • تحسينات منخفضة المستوى.

يبقى تقديم تمثيل عالي المستوى في TANGO ، لاختيار محدد. تكمن المشكلة في أن العرض المنخفض المستوى في TANGO يشبه المجمّع ، الذي يصعب صيانته وتصحيحه. كيف يجب أن يبدو المترجم بالداخل؟ لفهم أفضل ، صنعت موزيلا برنامج HolyJit الخاص بها ؛ هناك أيضًا خيار لكتابة لغتك المصغرة للترجمة بين تمثيل عالي المستوى ومنخفض المستوى.



التنمية لا تزال جارية. حسنًا ، وكذلك حول كيفية عدم المبالغة في التحسين.

الجزء 3. الأفضل هو عدو الخير


تجميع كما هو


عملية التحسين في Ion ، عندما يسخن الكود ثم يتم تجميعه وتحسينه ، جشع ، يمكن رؤيته في المثال التالي.

function foo(a, b) {
  return a + b;
}

function doSomeStuff(obj) {
  for (let i = 0; i < 1100; ++i) {
    print(foo(obj,obj));
  }
}

doSomeStuff("HollyJS");
doSomeStuff({n:10});

JS Shell ( ),    Mozilla, :



. , , - bailout (). , . foo object, , , . , :

function doSomeStuff(obj) {
  for (let i=0; i < 1100; ++i) {
    if (!(obj instanceof String))
      // bailout
    print(foo_only_str(obj, obj));
  }
}

, .

. , , DCE.




, , , .

, , , SpiderMonkey Resume Point. - , . , baseline . , runtime , . lowering, regAlloc, (snapshot), , . baseline .

:



runtime x86 : , . . , , , , , . , , Type . :



, , chain . , , .

: , chain-, N , , baseline, .  

, .

:


Ion 4- baseline. :




, , SpiderMonkey, V8 Node. — . .

. , , chain-.

, : 24-25 HolyJS, . — , .

Source: https://habr.com/ru/post/ar419155/


All Articles