عيوب المترجم JIT: لماذا يجب تجنب ميزات NYI؟

API7.ai

September 30, 2022

OpenResty (NGINX + Lua)

في المقال السابق، نظرنا إلى FFI في LuaJIT. إذا كان مشروعك يستخدم فقط واجهة برمجة التطبيقات (API) التي يوفرها OpenResty ولا تحتاج إلى استدعاء دوال C، فإن FFI ليس بهذه الأهمية بالنسبة لك. كل ما تحتاج إليه هو التأكد من تمكين lua-resty-core.

لكن NYI في LuaJIT، والذي سنتحدث عنه اليوم، هو مشكلة حاسمة لا يمكن لأي مهندس يستخدم OpenResty تجنبها، حيث تؤثر بشكل كبير على الأداء.

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

ما هو NYI؟

لنبدأ بتذكر نقطة ذكرناها سابقًا.

بيئة تشغيل LuaJIT، بالإضافة إلى تنفيذ مترجم Lua بلغة التجميع، تحتوي على مترجم JIT يمكنه توليد كود الآلة مباشرة.

تنفيذ مترجم JIT في LuaJIT ليس مكتملًا بعد. لا يمكنه ترجمة بعض الدوال لأنها صعبة التنفيذ ولأن مؤلفي LuaJيتقاعدون حاليًا بشكل شبه كامل. وتشمل هذه الدوال الشائعة مثل pairs()، unpack()، الوحدات النمطية C في Lua المبنية على تنفيذ CFunction في Lua، وهكذا. هذا يسمح لمترجم JIT بالعودة إلى وضع المترجم عند مواجهة عملية لا يدعمها في مسار الكود الحالي.

يحتوي الموقع الرسمي لـ LuaJIT على قائمة كاملة بهذه NYIs، وأقترح عليك الاطلاع عليها. الهدف من المقال ليس حفظ هذه القائمة، بل أن تذكر نفسك بها بوعي عند كتابة الكود.

فيما يلي، أخذت بعض الدوال من قائمة NYI لمكتبة النصوص.

مكتبة النصوص

حالة الترجمة لـ string.byte هي نعم، مما يعني أنه يمكن تحسينها باستخدام JIT، ويمكنك استخدامها في كودك دون خوف.

حالة الترجمة لـ string.char هي 2.1، مما يعني أنها مدعومة منذ LuaJIT 2.1. كما نعلم، LuaJIT في OpenResty مبني على LuaJIT 2.1، لذا يمكنك استخدامها بأمان.

حالة الترجمة لـ string.dump هي أبدًا، أي أنها لن تُحسن باستخدام JIT وستعود إلى وضع المترجم. حتى الآن، لا توجد خطط لدعمها في المستقبل.

string.find لديها حالة ترجمة 2.1 جزئيًا، مما يعني أنها مدعومة جزئيًا منذ LuaJIT 2.1، والملاحظة بعد ذلك تقول إنها تدعم فقط البحث عن النصوص الثابتة، وليس مطابقة الأنماط. لذا للعثور على النصوص الثابتة، يمكن تحسين string.find باستخدام JIT.

بطبيعة الحال، يجب علينا تجنب استخدام NYI حتى يتم ترجمة المزيد من كودنا باستخدام JIT وضمان الأداء. ومع ذلك، في بيئة حقيقية، نحتاج أحيانًا بشكل حتمي إلى استخدام بعض دوال NYI، فماذا نفعل؟

بدائل NYI

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

string.gsub()

لننظر أولاً إلى دالة string.gsub()، وهي دالة معالجة النصوص المدمجة في Lua التي تقوم بالاستبدال العام للنصوص، مثل المثال التالي.

$ resty -e 'local new = string.gsub("banana", "a", "A"); print(new)'
bAnAnA

هذه الدالة هي دالة NYI ولا يمكن ترجمتها بواسطة JIT.

يمكننا محاولة العثور على دالة بديلة في واجهة برمجة التطبيقات (API) الخاصة بـ OpenResty، ولكن بالنسبة لمعظم الناس، ليس عمليًا تذكر جميع واجهات برمجة التطبيقات واستخداماتها. لهذا السبب، أفتح دائمًا صفحة وثائق lua-nginx-module على GitHub في عملي التطويري.

على سبيل المثال، يمكننا استخدام gsub ككلمة مفتاحية للبحث في صفحة الوثائق، وسيتبادر إلى الذهن ngx.re.gsub.

يمكننا أيضًا استخدام أداة restydoc الموصى بها للبحث في واجهة برمجة التطبيقات (API) الخاصة بـ OpenResty. يمكنك تجربة استخدامها للبحث عن gsub.

$ restydoc -s gsub

كما ترى، بدلاً من إرجاع ngx.re.gsub الذي كنا نتوقعه، يتم عرض دوال Lua. في الواقع، في هذه المرحلة، restydoc تُرجع تطابقًا فريدًا دقيقًا، لذا فهي أكثر ملاءمة للاستخدام إذا كنت تعرف اسم واجهة برمجة التطبيقات (API) بشكل صريح. للبحث الغامض، لا يزال عليك القيام به يدويًا في الوثائق.

بالعودة إلى نتائج البحث، نرى أن تعريف دالة ngx.re.gsub هو كما يلي:

newstr, n, err = ngx.re.gsub(subject, regex, replace, options?)

هنا، يتم تسمية معاملات الدالة وقيم الإرجاع بمعاني محددة. في الواقع، في OpenResty، لا أنصحك بكتابة الكثير من التعليقات. في معظم الأحيان، الاسم الجيد أفضل من عدة أسطر من التعليقات.

بالنسبة للمهندسين غير المألوفين بنظام التعبيرات العادية في OpenResty، قد تشعر بالارتباك عند رؤية المتغير options في النهاية. ومع ذلك، شرح المتغير ليس في هذه الدالة بل في وثائق دالة ngx.re.match.

إذا نظرت إلى وثائق options، ستجد أنه إذا قمنا بتعيينها إلى jo، فإنها تُفعّل PCRE JIT، بحيث يمكن ترجمة الكود الذي يستخدم ngx.re.gsub باستخدام JIT من LuaJIT وكذلك باستخدام PCRE JIT.

لن أخوض في تفاصيل الوثائق. وثائق OpenResty ممتازة، لذا اقرأها بعناية ويمكنك حل معظم مشاكلك.

string.find()

على عكس string.gsub، يمكن ترجمة string.find باستخدام JIT في الوضع العادي (أي البحث عن النصوص)، بينما لا يمكن ترجمة string.find باستخدام JIT للبحث عن النصوص باستخدام التعبيرات العادية، والذي يتم باستخدام واجهة برمجة التطبيقات (API) الخاصة بـ OpenResty ngx.re.find.

لذلك، عند إجراء بحث عن نص في OpenResty، يجب أولاً التمييز بوضوح ما إذا كنت تبحث عن نص ثابت أو تعبير عادي. إذا كان الأول، استخدم string.find وتذكر تعيين plain إلى true في النهاية.

string.find("foo bar", "foo", 1, true)

في الحالة الثانية، يجب استخدام واجهة برمجة التطبيقات (API) الخاصة بـ OpenResty وتفعيل خيار JIT لـ PCRE.

ngx.re.find("foo bar", "^foo", "jo")

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

unpack()

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

$ resty -e '
 local a = {100, 200, 300, 400}
 for i = 1, 2 do
    print(unpack(a))
 end'

$ resty -e 'local a = {100, 200, 300, 400}
 for i = 1, 2 do
    print(a[1], a[2], a[3], a[4])
 end'

لنغوص أعمق قليلاً في unpack، وهذه المرة يمكننا استخدام restydoc للبحث.

$ restydoc -s unpack

كما ترى من وثائق unpack، unpack(list [, i [, j]]) تعادل return list[i], list[i+1], list[j]، ويمكنك التفكير في unpack كسكر نحوي. بهذه الطريقة، يمكنك الوصول إليها تمامًا مثل فهرس الصفيف دون كسر ترجمة JIT في LuaJIT.

pairs()

أخيرًا، لننظر إلى دالة pairs() التي تعبر جدول التجزئة، والتي أيضًا لا يمكن ترجمتها بواسطة JIT.

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

بعد ذكر هذه الأمثلة الأربعة، لنلخص أنه لتجنب استخدام دوال NYI، تحتاج إلى الانتباه إلى هاتين النقطتين.

  • استخدم واجهة برمجة التطبيقات (API) التي يوفرها OpenResty بدلاً من دوال المكتبة القياسية في Lua. تذكر أن Lua هي لغة مدمجة، ونحن نبرمج في OpenResty، وليس Lua.
  • إذا اضطررت إلى استخدام لغة NYI كملاذ أخير، يرجى التأكد من أنها ليست في مسار الكود الساخن.

كيف تكتشف NYI؟

كل هذا الحديث عن تجنب NYI هو لتعليمك ما يجب فعله. ومع ذلك، سيكون غير متسق مع إحدى الفلسفات التي يدعو إليها OpenResty إذا انتهى هنا بشكل مفاجئ.

ما يمكن أن تفعله الآلة تلقائيًا لا يتضمن البشر.

البشر ليسوا آلات، وسيكون هناك دائمًا أخطاء. أتمتة اكتشاف NYI المستخدمة في الكود هي انعكاس أساسي لقيمة المهندس.

هنا أوصي بوحدتي jit.dump و jit.v المدمجتين في LuaJIT. كلاهما يطبع عملية عمل مترجم JIT. الأول يخرج معلومات مفصلة يمكن استخدامها لتصحيح LuaJIT نفسها. يمكنك الرجوع إلى كودها المصدر لفهم أعمق؛ بينما يخرج الثاني معلومات أكثر بساطة، حيث كل سطر يتوافق مع أثر، وعادةً ما يستخدم للتحقق مما إذا كان يمكن ترجمته باستخدام JIT.

كيف يجب أن نفعل ذلك؟ يمكننا البدء بإضافة السطرين التاليين من الكود إلى init_by_lua.

local v = require "jit.v"
v.on("/tmp/jit.log")

ثم، قم بتشغيل أداة اختبار الضغط أو بضع مئات من مجموعات اختبار الوحدة لجعل LuaJIT ساخنًا بما يكفي لتحفيز ترجمة JIT. بمجرد الانتهاء، تحقق من نتائج /tmp/jit.log.

بالطبع، هذه الطريقة مرهقة نسبيًا، لذا إذا كنت تريد تبسيط الأمور، resty كافية، وأداة سطر الأوامر الخاصة بـ OpenResty تأتي مع الخيارات التالية.

$resty -j v -e 'for i=1, 1000 do
      local newstr, n, err = ngx.re.gsub("hello, world", "([a-z])[a-z]+", "[$0,$1]", "i")
 end'
 [TRACE   1 (command line -e):1 stitch C:107bc91fd]
 [TRACE   2 (1/stitch) (command line -e):2 -> 1]

حيث -j في resty هو الخيار المتعلق بـ LuaJIT، والقيم dump و v تتبع، وتتوافق مع تفعيل وضع jit.dump و jit.v.

في إخراج وحدة jit.v، كل سطر هو كائن أثر تم ترجمته بنجاح. للتو كان مثالًا على أثر يمكن ترجمته باستخدام JIT، وإذا تمت مواجهة دوال NYI، سيحدد الإخراج أنها NYIs، كما في مثال pairs التالي.

$resty -j v -e 'local t = {}
 for i=1,100 do
     t[i] = i
 end

 for i=1, 1000 do
     for j=1,1000 do
         for k,v in pairs(t) do
             --
         end
     end
 end'

لا يمكن ترجمتها باستخدام JIT، لذا تشير النتيجة إلى دالة NYI في السطر 8.

 [TRACE   1 (command line -e):2 loop]
 [TRACE --- (command line -e):7 -- NYI: bytecode 72 at (command line -e):8]

ختامًا

هذه هي المرة الأولى التي نتحدث فيها عن مشكلات أداء OpenResty بشكل أطول. بعد قراءة هذه التحسينات حول NYI، ما رأيك؟ يمكنك ترك تعليق برأيك.

أخيرًا، سأترك لك سؤالًا محفزًا للتفكير عند مناقشة بدائل دالة string.find()؛ ذكرت أنه سيكون من الأفضل عمل طبقة تغليف وتفعيل خيارات التحسين بشكل افتراضي. لذا، سأترك هذه المهمة لك لتجربة قيادة صغيرة.

لا تتردد في كتابة إجاباتك في قسم التعليقات، ومرحبًا بك في مشاركة هذه المقالة مع زملائك وأصدقائك للتواصل والتقدم معًا.