اعتماد أداء التعليمات البرمجية على سياق تعريف متغير في JavaScript


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

في نهاية مقالي "باستخدام السماح للإعلانات والميزات المتغيرة لإغلاقات جافا سكريبت الناتجة" ، لمست بإيجاز مقارنة أداء الإعلانات (و LexicalDeclaration) و var (VarDeclaredNames) في الحلقات. للمقارنة ، استخدمنا وقت التشغيل اليدوي (بدون مساعدة من Array.prototype.sort () ) لفرز المصفوفة ، واحدة من أبسط الطرق هي الفرز حسب الاختيار ، نظرًا لأن طول الصفيف 100000 وصلنا إلى أكثر من 5 مليارات. التكرار في دورتين (الخارجية والمتداخلة) ، وهذا المبلغ يجب أن يسمح بتقدير كاف في النهاية.

بالنسبة لـ var ، كان يتم فرز العرض:

for (var i = 0, len = arr.length; i < len-1; i++) { var min, mini = i; for (var j = i+1; j < len; j++) { if (arr[mini] > arr[j]) mini = j; } min = arr[mini]; arr[mini] = arr[i]; arr[i] = min; } //   Firefox: 9.082 . //   Chrome: 10.783 . 

ولندع :

 for (let i = 0, len = arr.length; i < len-1; i++) { let min, mini = i; for (let j = i+1; j < len; j++) { if (arr[mini] > arr[j]) mini = j; } min = arr[mini]; arr[mini] = arr[i]; arr[i] = min; } //   Firefox: 5.261 . //   Chrome: 5.391 . 

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

ولكن ، قبل القيام بذلك ، تحتاج إلى الخوض في عمل الحلقة من أجل التعمق ، مسترشداً بالمواصفات الحالية لـ ECMAScript 2019 (ECMA-262) :

 13.7.4.7Runtime Semantics: LabelledEvaluation With parameter labelSet. IterationStatement':'for(Expression;Expression;Expression)Statement 1. If the first Expression is present, then a. Let exprRef be the result of evaluating the first Expression. b. Perform ? GetValue(exprRef). 2. Return ? ForBodyEvaluation(the second Expression, the third Expression, Statement, « », labelSet). IterationStatement':'for(varVariableDeclarationList;Expression;Expression)Statement 1. Let varDcl be the result of evaluating VariableDeclarationList. 2. ReturnIfAbrupt(varDcl). 3. Return ? ForBodyEvaluation(the first Expression, the second Expression, Statement, « », labelSet). IterationStatement':'for(LexicalDeclarationExpression;Expression)Statement 1. Let oldEnv be the running execution context's LexicalEnvironment. 2. Let loopEnv be NewDeclarativeEnvironment(oldEnv). 3. Let loopEnvRec be loopEnv's EnvironmentRecord. 4. Let isConst be the result of performing IsConstantDeclaration of LexicalDeclaration. 5. Let boundNames be the BoundNames of LexicalDeclaration. 6. For each element dn of boundNames, do a. If isConst is true, then i. Perform ! loopEnvRec.CreateImmutableBinding(dn, true). b. Else, i. Perform ! loopEnvRec.CreateMutableBinding(dn, false). 7. Set the running execution context's LexicalEnvironment to loopEnv. 8. Let forDcl be the result of evaluating LexicalDeclaration. 9. If forDcl is an abrupt completion, then a. Set the running execution context's LexicalEnvironment to oldEnv. b. Return Completion(forDcl). 10. If isConst is false, let perIterationLets be boundNames; otherwise let perIterationLets be « ». 11. Let bodyResult be ForBodyEvaluation(the first Expression, the second Expression, Statement, perIterationLets, labelSet). 12. Set the running execution context's LexicalEnvironment to oldEnv. 13. Return Completion(bodyResult). 
ملاحظة: لا يتم تأطير الرموز النقطية بعد IterationStatements ، في المصدر بواسطة الفواصل العليا - هنا بحيث لا يوجد تنسيق تلقائي يفسد إلى حد كبير قابلية قراءة النص.

هنا ، كما نرى ، هناك ثلاثة خيارات للاتصال والعمل الإضافي للحلقة:
  • مع ل (التعبير ؛ التعبير ؛ التعبير) بيان
    ForBodyEvaluation (التعبير الثاني ، التعبير الثالث ، بيان ، "" ، labelSet) .
  • مع ل (varVariableDeclarationList ؛ التعبير ؛ التعبير) بيان
    ForBodyEvaluation (التعبير الأول ، التعبير الثاني ، بيان ، "" ، labelSet).
  • في ل (LexicalDeclarationExpression ، التعبير) بيان
    ForBodyEvaluation (التعبير الأول ، التعبير الثاني ، البيان ، perIterationLets ، labelSet)

في المتغير الأخير ، الثالث ، بخلاف الأولين ، المعلمة الرابعة ليست فارغة - perIterationLets - هذه هي في الواقع نفس التصريحات في المعلمة الأولى التي تم تمريرها إلى الحلقة. وهي محددة في الفقرة 10:
- إذا كانت isConst خاطئة ، فدع perIterationLets أن يكون ملزما بالأسماء ؛ وإلا فدع perIterationLets يكون "".
إذا تم تمرير ثابت إلى ، ولكن ليس متغير ، تصبح المعلمة perIterationLets فارغة.

أيضًا ، في الخيار الثالث ، من الضروري الانتباه إلى الفقرة 2:
- دع loopEnv تكون NewDeclarativeEnvironment (oldEnv).

 8.1.2.2NewDeclarativeEnvironment ( E ) When the abstract operation NewDeclarativeEnvironment is called with a Lexical Environment as argument E the following steps are performed: 1. Let env be a new Lexical Environment. 2. Let envRec be a new declarative Environment Record containing no bindings. 3. Set env's EnvironmentRecord to envRec. 4. Set the outer lexical environment reference of env to E. 5. Return env. 

هنا ، كمعلمة E ، يتم أخذ البيئة التي تم من خلالها تسمية حلقة for (عمومية ، أي وظيفة ، وما إلى ذلك) ، ويتم إنشاء بيئة جديدة لتنفيذ الحلقة مع الإشارة إلى البيئة الخارجية التي أنشأتها (النقطة 4). نحن مهتمون بهذه الحقيقة بسبب حقيقة أن البيئة هي سياق التنفيذ.

ونحن نتذكر أن دع و const الإعلانات المتغيرة مرتبطة من الناحية السياقية بالكتلة التي أعلنت فيها.

 13.2.14Runtime Semantics: BlockDeclarationInstantiation ( code, env ) Note When a Block or CaseBlock is evaluated a new declarative Environment Record is created and bindings for each block scoped variable, constant, function, or class declared in the block are instantiated in the Environment Record. BlockDeclarationInstantiation is performed as follows using arguments code and env. code is the Parse Node corresponding to the body of the block. env is the Lexical Environment in which bindings are to be created. 1. Let envRec be env's EnvironmentRecord. 2. Assert: envRec is a declarative Environment Record. 3. Let declarations be the LexicallyScopedDeclarations of code. 4. For each element d in declarations, do a. For each element dn of the BoundNames of d, do i. If IsConstantDeclaration of d is true, then 1. Perform ! envRec.CreateImmutableBinding(dn, true). ii. Else, 1. Perform ! envRec.CreateMutableBinding(dn, false). b. If d is a FunctionDeclaration, a GeneratorDeclaration, an AsyncFunctionDeclaration, or an AsyncGeneratorDeclaration, then i. Let fn be the sole element of the BoundNames of d. ii. Let fo be the result of performing InstantiateFunctionObject for d with argument env. iii. Perform envRec.InitializeBinding(fn, fo). 

ملاحظة: نظرًا لأن الخيارين الأولين من استدعاء for for loop لم يكن هناك مثل هذه الإعلانات ، لم تكن هناك حاجة لإنشاء بيئة جديدة لهما.

نذهب أبعد من ذلك ونفكر في ForBodyEvaluation :

 13.7.4.8Runtime Semantics: ForBodyEvaluation ( test, increment, stmt, perIterationBindings, labelSet ) The abstract operation ForBodyEvaluation with arguments test, increment, stmt, perIterationBindings, and labelSet is performed as follows: 1. Let V be undefined. 2. Perform ? CreatePerIterationEnvironment(perIterationBindings). 3. Repeat, a. If test is not [empty], then i. Let testRef be the result of evaluating test. ii. Let testValue be ? GetValue(testRef). iii. If ToBoolean(testValue) is false, return NormalCompletion(V). b. Let result be the result of evaluating stmt. c. If LoopContinues(result, labelSet) is false, return Completion(UpdateEmpty(result, V)). d. If result.[[Value]] is not empty, set V to result.[[Value]]. e. Perform ? CreatePerIterationEnvironment(perIterationBindings). f. If increment is not [empty], then i. Let incRef be the result of evaluating increment. ii. Perform ? GetValue(incRef). 

ما يجب عليك الانتباه إليه أولاً:
  • وصف المعلمات الواردة:
    • اختبار : تم التحقق من التعبير عن الحقيقة قبل التكرار التالي لجسم الحلقة (على سبيل المثال: i <len ) ؛
    • الزيادة : تعبير يتم تقييمه في بداية كل تكرار جديد (باستثناء الأول) (على سبيل المثال: i ++ ) ؛
    • stmt : حلقة الجسم
    • perIterationBindings : المتغيرات التي تم الإعلان عنها مع السماح بإدخال المعلمة أولاً (على سبيل المثال: let i = 0 ||
    • labelSet : تسمية الحلقة ؛
  • النقطة 2: هنا ، إذا تم تمرير المعلمة غير الفارغة perIterationBindings ، يتم إنشاء بيئة ثانية للقيام بالمرور الأولي للحلقة ؛
  • الفقرة 3. أ: التحقق من وجود شرط معين لمواصلة تنفيذ الدورة ؛
  • المادة 3.ب: تنفيذ جسم الدورة ؛
  • النقطة 3.e: خلق بيئة جديدة.

حسنًا ، وبشكل مباشر ، الخوارزمية لإنشاء بيئات داخلية للحلقة:

 13.7.4.9Runtime Semantics: CreatePerIterationEnvironment ( perIterationBindings ) 1. The abstract operation CreatePerIterationEnvironment with argument perIterationBindings is performed as follows: 1. If perIterationBindings has any elements, then a. Let lastIterationEnv be the running execution context's LexicalEnvironment. b. Let lastIterationEnvRec be lastIterationEnv's EnvironmentRecord. c. Let outer be lastIterationEnv's outer environment reference. d. Assert: outer is not null. e. Let thisIterationEnv be NewDeclarativeEnvironment(outer). f. Let thisIterationEnvRec be thisIterationEnv's EnvironmentRecord. g. For each element bn of perIterationBindings, do i. Perform ! thisIterationEnvRec.CreateMutableBinding(bn, false). ii. Let lastValue be ? lastIterationEnvRec.GetBindingValue(bn, true). iii. Perform thisIterationEnvRec.InitializeBinding(bn, lastValue). h. Set the running execution context's LexicalEnvironment to thisIterationEnv. 2. Return undefined. 

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

كمثال ، ضع في الاعتبار تعبير مشابه:

 let arr = []; for (let i = 0; i < 3; i++) { arr.push(i); } console.log(arr); // Array(3) [ 0, 1, 2 ] 

وهنا كيف يمكن أن تتحلل دون استخدام ل (مع قدر معين من الاصطلاحية):

 let arr = []; //    { let i = 0; //     for } //   ,   { let i = 0; //    i    if (i < 3) arr.push(i); } //    { let i = 0; //    i    i++; if (i < 3) arr.push(i); } //    { let i = 1; //    i    i++; if (i < 3) arr.push(i); } //    { let i = 2; //    i    i++; if (i < 3) arr.push(i); } console.log(arr); // Array(3) [ 0, 1, 2 ] 

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

إليك كيف ، على سبيل المثال ، ستبدو هذه الحلقة عند استخدام var عند عدم وجود روابط إضافية:

 let arr2 = []; var i = 0; if (i < 3) arr.push(i); i++; if (i < 3) arr.push(i); i++; if (i < 3) arr.push(i); i++; if (i < 3) arr.push(i); console.log(arr); // Array(3) [ 0, 1, 2 ] 

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

دعونا نحاول القيام بذلك ، باستخدام نفس الفرز لمجموعة من 100000 عنصر كمثال ، ومن أجل الجمال ، نقوم أيضًا بتعريف جميع المتغيرات الأخرى من قبل من أجل :

 let i, j, min, mini, len = arr.length; for (i = 0; i < len-1; i++) { mini = i; for (j = i+1; j < len; j++) { if (arr[mini] > arr[j]) mini = j; } min = arr[mini]; arr[mini] = arr[i]; arr[i] = min; } //   Firefox: 34.246 . //   Chrome: 10.803 . 

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

تقريبا. هذا لم ينجح ، دعنا نرجع إعلان المتغيرات i و j مرة أخرى إلى معلمات الدورات المقابلة:

 let min, mini, len = arr.length; for (let i = 0; i < len-1; i++) { mini = i; for (let j = i+1; j < len; j++) { if (arr[mini] > arr[j]) mini = j; } min = arr[mini]; arr[mini] = arr[i]; arr[i] = min; } //   Firefox: 6.575 . //   Chrome: 6.749 . 

هم. من الناحية الفنية ، يبدو أن الاختلاف الوحيد بين المثال الأخير والمثال في بداية المقالة هو التصريحات الصادرة عن المتغيرات min و mini و len خارج الحلقة for ، وعلى الرغم من أن الفرق لا يزال سياقيًا ، إلا أنه لا يمثل أهمية خاصة لنا الآن ، وبالإضافة إلى ذلك ، لقد تخلصنا من الحاجة إلى الإعلان عن هذه المتغيرات 99،999 مرة في جسم دورة المستوى العلوي ، والتي من الناحية النظرية ، ستزيد من الإنتاجية بدلاً من تقليلها بأكثر من ثانية.

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

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

 { let i, j, min, mini, len = arr.length; for (i = 0; i < len-1; i++) { mini = i; for (j = i+1; j < len; j++) { if (arr[mini] > arr[j]) mini = j; } min = arr[mini]; arr[mini] = arr[i]; arr[i] = min; } } //   Firefox: 5.262 . //   Chrome: 5.405 . 

فويلا! اتضح أن المشكلة كانت هي أن تدع الإعلانات تمت في سياق عالمي ، وبمجرد تخصيص كتلة منفصلة لهم ، اختفت جميع المشكلات هناك.

وهنا سيكون من الجميل أن نتذكر طريقة أخرى لعنة غير مستحقة قليلاً لإعلان المتغيرات - var .

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

 function test() { var i, j, min, mini, len = arr.length; for (i = 0; i < len-1; i++) { mini = i; for (j = i+1; j < len; j++) { if (arr[mini] > arr[j]) mini = j; } min = arr[mini]; arr[mini] = arr[i]; arr[i] = min; } } test(); //   Firefox: 5.255 . //   Chrome: 5.411 . 

وحصلنا على نتيجة مماثلة تقريبا لما كان عند استخدام اسمحوا .

أخيرًا ، دعونا نتحقق مما إذا كان التباطؤ يحدث من خلال قراءة المتغير العام دون تغيير قيمته.

سمح

 let len = arr.length; for (let i = 0; i < len-1; i++) { let min, mini = i; for (let j = i+1; j < len; j++) { if (arr[mini] > arr[j]) mini = j; } min = arr[mini]; arr[mini] = arr[i]; arr[i] = min; } //   Firefox: 5.262 . //   Chrome: 5.391 . 

فار

 var len = arr.length; function test() { var i, j, min, mini; for (i = 0; i < len-1; i++) { mini = i; for (j = i+1; j < len; j++) { if (arr[mini] > arr[j]) mini = j; } min = arr[mini]; arr[mini] = arr[i]; arr[i] = min; } } test(); //   Firefox: 5.258 . //   Chrome: 5.439 . 

تشير النتائج إلى أن قراءة المتغير العام لم تؤثر على وقت التنفيذ.

لتلخيص


  1. تغيير المتغيرات العالمية أبطأ بكثير من تغيير المتغيرات المحلية. مع مراعاة ذلك ، من الممكن تحسين الكود في المواقف المناسبة عن طريق إنشاء كتلة أو وظيفة منفصلة ، بما في ذلك إعلان المتغيرات ، بدلاً من تنفيذ جزء من الكود في سياق عام. نعم ، في أي كتاب مدرسي تقريبًا ، يمكنك العثور على توصيات لجعل أقل عدد ممكن من الروابط العالمية ممكنًا ، ولكن عادةً ما يشار فقط إلى انسداد مساحة الاسم العالمية كسبب وليس كلمة حول مشاكل الأداء المحتملة.
  2. على الرغم من حقيقة أن تنفيذ الحلقات مع إعلان السماح في الأول للمعلمة يخلق عددًا كبيرًا من البيئات ، فإن هذا لا يؤثر تقريبًا على الأداء ، على عكس المواقف التي نتخذ فيها مثل هذه الإعلانات خارج الكتلة. ومع ذلك ، ينبغي للمرء ألا يستبعد احتمال وجود حالات غريبة عندما يؤثر هذا العامل على الإنتاجية بشكل أكبر.
  3. لا يزال أداء المتغيرات var أدنى من أداء المتغيرات let ، ومع ذلك ، فإنه لا يتجاوزها (مرة أخرى ، في الحالة العامة) ، مما يؤدي بنا إلى الاستنتاج التالي بأنه لا يوجد سبب لاستخدام تعريفات var إلا لأغراض التوافق. ومع ذلك ، إذا كنت بحاجة إلى التعامل مع المتغيرات العامة مع تغيير قيمها ، فسيكون متغير var الأفضل من حيث الأداء (على الأقل في الوقت الحالي ، إذا كان ، على وجه الخصوص ، من المفترض أنه يمكن أيضًا تشغيل البرنامج النصي على محرك Gecko).

مراجع


ECMAScript 2019 (ECMA-262)
استخدام دع بيانات التعريفات للمتغيرات وميزات عمليات الإغلاق الناتجة في JavaScript

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


All Articles