لاحظت أن الناس يستخدمون البناء في الغالب مثل هذا:
var length = array.Length; for (int i = 0; i < length; i++) {
يعتقدون أن إجراء مكالمة إلى Array.Length على كل تكرار سيجعل CLR يستغرق وقتًا أطول لتنفيذ التعليمات البرمجية. لتجنب ذلك ، يقومون بتخزين قيمة الطول في متغير محلي.
دعنا نتعرف (مرة واحدة وإلى الأبد!) إذا كان هذا أمرًا قابلاً للتطبيق أو أن استخدام متغير مؤقت يعد مضيعة للوقت.
للبدء ، دعنا نتفحص طرق C #:
public int WithoutVariable() { int sum = 0; for (int i = 0; i < array.Length; i++) { sum += array[i]; } return sum; } public int WithVariable() { int sum = 0; int length = array.Length; for (int i = 0; i < length; i++) { sum += array[i]; } return sum; }
فيما يلي كيفية معالجتها بواسطة برنامج التحويل البرمجي JIT (لبرنامج .NET Framework 4.7.2 ضمن LegacyJIT-x86):
من السهولة ملاحظة أن لديهم نفس العدد بالضبط من إرشادات المجمّع - 15. حتى منطق هذه التعليمات هو نفسه تقريبًا. هناك اختلاف بسيط في ترتيب تهيئة المتغيرات والمقارنات حول ما إذا كان ينبغي استمرار الدورة. يمكننا ملاحظة أنه في كلتا الحالتين يتم تسجيل طول الصفيف مرتين قبل الدورة:
- للتحقق من 0 (arrayLength)
- في المتغير المؤقت لفحص حالة الدورة (arrayLength2).
اتضح أن كلتا الطريقتين ستترجمان إلى نفس الرمز بالضبط ، لكن أولهما يتم كتابته بشكل أسرع ، على الرغم من عدم وجود فائدة فيما يتعلق بوقت التنفيذ.
قادني رمز المجمّع أعلاه إلى بعض الأفكار وقررت التحقق من عدة طرق أخرى:
public int WithoutVariable() { int sum = 0; for(int i = 0; i < array.Length; i++) { sum += array[i] + array.Length; } return sum; } public int WithVariable() { int sum = 0; int length = array.Length; for(int i = 0; i < length; i++) { sum += array[i] + length; } return sum; }
تتم الآن إضافة العنصر الحالي وطول الصفيف ، ولكن في الحالة الأولى يتم طلب طول الصفيف في كل مرة ، وفي الحالة الثانية يتم حفظه مرة واحدة في متغير محلي. دعنا نلقي نظرة على كود المجمع لهذه الطرق:
مرة أخرى ، عدد التعليمات هي نفسها ، وكذلك (تقريبا) التعليمات نفسها. الاختلاف الوحيد هو ترتيب تهيئة المتغيرات وشروط التحقق لاستمرار الدورة. يمكنك ملاحظة أنه في حساب المجموع ، يتم أخذ الطول الأول للصفيف فقط في الاعتبار. من الواضح أن هذا:
int arrayLength2 = localRefToArray . طول
mov edi ، dword ptr [ edx + 4 ]
إذا كان ( i> = arrayLength2 ) يرمي IndexOutOfRangeException ( ) جديدًا ؛
cmp ecx edi
جاي 04b12d39
في جميع الطرق الأربعة ، يتم التحقق من حدود الصفيف المضمن ويتم تنفيذها لكل عنصر من عناصر الصفيف.
يمكننا بالفعل الوصول إلى الاستنتاج الأول: استخدام متغير إضافي لمحاولة تسريع الدورة هو مضيعة للوقت ، لأن المترجم سيقوم بذلك نيابةً عنك. السبب الوحيد لتخزين صفيف طول في متغير هو جعل التعليمات البرمجية أكثر قابلية للقراءة.
ForEach هو موقف آخر تماما. خذ بعين الاعتبار الطرق الثلاثة التالية:
public int ForEachWithoutLength() { int sum = 0; foreach (int i in array) { sum += i; } return sum; } public int ForEachWithLengthWithoutLocalVariable() { int sum = 0; foreach (int i in array) { sum += i + array.Length; } return sum; } public int ForEachWithLengthWithLocalVariable() { int sum = 0; int length = array.Length; foreach (int i in array) { sum += i + length; } return sum; }
وإليك الرمز بعد JIT:
ForEachWithoutLength ()؛ int sum = 0 ؛
xor esi ، esi
؛ int [] localRefToArray = this.array؛
mov ecx ، dword ptr [ ecx + 4 ]
؛ كثافة العمليات = 0 ؛
xor edx ، edx
؛ int arrayLength = localRefToArray.Length؛
mov edi ، dword ptr [ ecx + 4 ]
؛ إذا كان (arrayLength == 0) goto exit ؛
اختبار edi edi
خروج jle
؛ كثافة العمليات ر = صفيف [i] ؛
حلقة :
mov eax ، dword ptr [ ecx + edx * 4 + 8 ]
؛ مبلغ + = أنا ؛
أضف esi ، eax
؛ أنا ++ ؛
المؤتمر الوطني العراقي
؛ إذا (أنا <arrayLength) حلقة الذهاب
cmp edi edx
حلقة jg
؛ مبلغ الإرجاع ؛
الخروج :
وسائل التحقق eax ، esi
ForEachWithLengthWithoutLocalVariable ()؛ int sum = 0 ؛
xor esi ، esi
؛ int [] localRefToArray = this.array؛
mov ecx ، dword ptr [ ecx + 4 ]
؛ كثافة العمليات = 0 ؛
xor edx ، edx
؛ int arrayLength = localRefToArray.Length؛
mov edi ، dword ptr [ ecx + 4 ]
؛ إذا كان (arrayLength == 0) غوتو الخروج
اختبار edi edi
خروج jle
؛ كثافة العمليات ر = صفيف [i] ؛
حلقة :
mov eax ، dword ptr [ ecx + edx * 4 + 8 ]
؛ مبلغ + = أنا ؛
أضف esi ، eax
؛ sum + = localRefToArray.Length؛
أضف esi ، dword ptr [ ecx + 4 ]
؛ أنا ++ ؛
المؤتمر الوطني العراقي
؛ إذا (أنا <arrayLength) حلقة الذهاب
cmp edi edx
حلقة jg
؛ مبلغ الإرجاع ؛
الخروج :
وسائل التحقق eax ، esi
ForEachWithLengthWithLocalVariable ()؛ int sum = 0 ؛
xor esi ، esi
؛ int [] localRefToArray = this.array؛
mov edx ، dword ptr [ ecx + 4 ]
؛ int length = localRefToArray.Length؛
mov ebx ، dword ptr [ edx + 4 ]
؛ كثافة العمليات = 0 ؛
xor ecx ، ecx
؛ int arrayLength = localRefToArray.Length؛
mov edi ، dword ptr [ edx + 4 ]
؛ إذا كان (arrayLength == 0) goto exit ؛
اختبار edi edi
خروج jle
؛ كثافة العمليات ر = صفيف [i] ؛
حلقة :
mov eax ، dword ptr [ edx + ecx * 4 + 8 ]
؛ مبلغ + = أنا ؛
أضف esi ، eax
؛ مجموع + = الطول ؛
أضف esi ، ebx
؛ أنا ++ ؛
inc ecx
؛ إذا (أنا <arrayLength) حلقة الذهاب
cmp edi ecx
حلقة jg
؛ مبلغ الإرجاع ؛
الخروج :
وسائل التحقق eax ، esi
أول ما يتبادر إلى الذهن هو أنه يتطلب تعليمات مجمّع أقل من دورة التجميع (على سبيل المثال ، لتلخيص العناصر البسيطة ، استغرق الأمر 12 تعليمة في
foreach ، ولكن 15 في
for ).
بشكل عام ، فيما يلي نتائج
لـ vs foreach القياسي لـ 1 مليون عنصر:
sum+=array[i];
و من أجل
sum+=array[i] + array.Length;
يسير
ForEach عبر الصفيف بشكل أسرع بكثير من
أجل . لماذا؟ لمعرفة ذلك ، نحتاج إلى مقارنة الكود بعد JIT:
مقارنة بين جميع خيارات foreach الثلاثة دعونا نلقي نظرة على ForEachWithoutLength. يتم طلب طول الصفيف مرة واحدة فقط ولا توجد أية اختبارات لحدود الصفيف. يحدث ذلك لأن دورة ForEach تقيد أولاً تغيير المجموعة داخل الدورة ، والثانية لن تخرج عن المجموعة. بسبب ذلك ، يمكن JIT تحمل لإزالة حدود مجموعة الشيكات.
الآن دعونا ننظر بعناية في ForEachWithLengthWIthoutLocalVariable. لا يوجد سوى جزء واحد غريب ، حيث يحدث sum + = length ليس للصفيف المتغير المحلي المحفوظ مسبقًا ، ولكن إلى جزء جديد يطلبه التطبيق من الذاكرة في كل مرة. هذا يعني أنه ستكون هناك طلبات ذاكرة N + 1 لطول الصفيف ، حيث N هو طول صفيف.
والآن نأتي إلى ForEachWithLengthWithLocalVariable. الكود هناك هو نفسه تمامًا كما في المثال السابق ، باستثناء معالجة طول الصفيف. قام المحول البرمجي مرة أخرى بإنشاء صفيف متغير محلي طوله يُستخدم للتحقق مما إذا كانت المصفوفة فارغة ، لكن المحول البرمجي ما زال يحفظ بصدق طول المتغير المحلي المعلن لدينا ، وهذا ما يتم استخدامه في الملخص داخل الدورة. اتضح أن هذه الطريقة تطلب طول الصفيف من الذاكرة مرتين فقط. الفرق صعب للغاية في العالم الحقيقي.
في جميع الحالات ، تحولت أداة التجميع إلى رمز بسيط للغاية لأن الأساليب نفسها بسيطة. إذا كانت الأساليب تحتوي على مزيد من المعلمات ، فسيتعين عليها التعامل مع الحزمة ، فقد تحصل المتغيرات على مخازن خارج السجلات ، وقد يكون هناك المزيد من الاختبارات ، ولكن المنطق الرئيسي سيبقى كما هو: إدخال متغير محلي لطول الصفيف هو فقط مفيدة لصنع رمز أكثر قابلية للقراءة. اتضح أيضًا أن
Foreach غالبًا ما يمر عبر الصفيف بشكل أسرع من
For .