يجب أن يتم تخزين طول مجموعة في متغير محلي في C #؟

لاحظت أن الناس يستخدمون البناء في الغالب مثل هذا:

var length = array.Length; for (int i = 0; i < length; i++) {    //do smth } 

يعتقدون أن إجراء مكالمة إلى 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):
بدون متغير ()
؛ int sum = 0 ؛
xor edi ، edi
؛ كثافة العمليات = 0 ؛
xor esi ، esi
؛ int [] localRefToArray = this.array؛
mov edx ، dword ptr [ ecx + 4 ]
؛ int arrayLength = localRefToArray.Length؛
mov ecx ، dword ptr [ edx + 4 ]
؛ إذا كان (arrayLength == 0) يُرجع المبلغ ؛
اختبار ecx ، ecx
خروج jle
؛ int arrayLength2 = localRefToArray.Length؛
mov eax ، dword ptr [ edx + 4 ]
؛ إذا (i> = arrayLength2)
. رمي IndexOutOfRangeException () جديد ؛
حلقة :
cmp esi ، eax
جاي 056e2d31
؛ sum + = localRefToArray [i]؛
إضافة edi ، dword ptr [ edx + esi * 4 + 8 ]
؛ أنا ++ ؛
المؤتمر الوطني العراقي
؛ إذا (أنا <arrayLength) حلقة الذهاب
cmp ecx ، esi
حلقة jg
؛ مبلغ الإرجاع ؛
الخروج :
وسائل التحقق eax ، edi
مع المتغير ()
؛ int sum = 0 ؛
xor esi ، esi
؛ int [] localRefToArray = this.array؛
mov edx ، dword ptr [ ecx + 4 ]
؛ int arrayLength = localRefToArray.Length؛
mov edi ، dword ptr [ edx + 4 ]
؛ كثافة العمليات = 0 ؛
إكسور إكس ، إكس
؛ إذا كان (arrayLength == 0) يُرجع المبلغ ؛
اختبار edi edi
خروج jle
؛ int arrayLength2 = localRefToArray.Length؛
mov ecx ، dword ptr [ edx + 4 ]
؛ إذا (i> = arrayLength2)
. رمي IndexOutOfRangeException () جديد ؛
حلقة :
cmp eax ، ecx
جاي 05902d31
؛ sum + = localRefToArray [i]؛
أضف esi ، dword ptr [ edx + eax * 4 + 8 ]
؛ أنا ++ ؛
inc eax
؛ إذا (أنا <arrayLength) حلقة الذهاب
cmp eax edi
حلقة jl
؛ مبلغ الإرجاع ؛
الخروج :
وسائل التحقق eax ، esi

مقارنة في ميلد:


من السهولة ملاحظة أن لديهم نفس العدد بالضبط من إرشادات المجمّع - 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 sum = 0 ؛
xor edi ، edi
int i = 0 ؛
xor esi ، esi
الباحث [ ] localRefToArray = هذا . مجموعة
mov edx ، dword ptr [ ecx + 4 ]
int arrayLength = localRefToArray . طول
mov ebx ، dword ptr [ edx + 4 ]
إذا كان ( arrayLength == 0 ) بإرجاع مبلغ ؛
اختبار ebx ، ebx
خروج jle
int arrayLength2 = localRefToArray . طول
mov ecx ، dword ptr [ edx + 4 ]
إذا ( i> = arrayLength2 )
رمي IndexOutOfRangeException ( ) جديد ؛
حلقة :
cmp esi ecx
جاي 05562d39
int t = array [ i ] ؛
mov eax ، dword ptr [ edx + esi * 4 + 8 ]
ر + = المبلغ ؛
إضافة eax ، EDI
t + = arrayLength ؛
أضف eax ، ebx
المبلغ = ر ؛
mov edi ، eax
أنا ++ ؛
المؤتمر الوطني العراقي
إذا ( أنا <arrayLength ) حلقة الذهاب
cmp ebx ، esi
حلقة jg
مبلغ الإرجاع
الخروج :
وسائل التحقق eax ، edi
مع المتغير ()
int sum = 0 ؛
xor esi ، esi
الباحث [ ] localRefToArray = هذا . مجموعة
mov edx ، dword ptr [ ecx + 4 ]
int arrayLength = localRefToArray . طول
mov ebx ، dword ptr [ edx + 4 ]
int i = 0 ؛
xor ecx ، ecx
إذا ( arrayLength == 0 ) ( مبلغ الإرجاع ؛)
اختبار ebx ، ebx
خروج jle
int arrayLength2 = localRefToArray . طول
mov edi ، dword ptr [ edx + 4 ]
إذا ( i> = arrayLength2 )
رمي IndexOutOfRangeException ( ) جديد ؛
حلقة :
cmp ecx edi
جاي 04b12d39
int t = array [ i ] ؛
mov eax ، dword ptr [ edx + ecx * 4 + 8 ]
ر + = المبلغ ؛
أضف eax ، esi
t + = arrayLength ؛
أضف eax ، ebx
المبلغ = ر ؛
وسائل التحقق esi ، eax
أنا ++ ؛
inc ecx
إذا ( أنا <arrayLength ) حلقة الذهاب
cmp ecx ، ebx
حلقة jl
مبلغ الإرجاع
الخروج :
وسائل التحقق eax ، esi

مقارنة في ميلد:


مرة أخرى ، عدد التعليمات هي نفسها ، وكذلك (تقريبا) التعليمات نفسها. الاختلاف الوحيد هو ترتيب تهيئة المتغيرات وشروط التحقق لاستمرار الدورة. يمكنك ملاحظة أنه في حساب المجموع ، يتم أخذ الطول الأول للصفيف فقط في الاعتبار. من الواضح أن هذا:
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]; 
طريقة
ItemsCount
متوسط
خطأ
StdDev
متوسط
نسبة
RatioSD
ForEach
1000000
1.401 مللي ثانية
0.2691 مللي ثانية
0.7935 مللي ثانية
1.694 مللي ثانية
1.00
0.00
إلى
1000000
1.586 مللي ثانية
0.3204 مللي ثانية
0.9447 مللي ثانية
1.740 مللي ثانية
1.23
0.65
و من أجل
 sum+=array[i] + array.Length; 
طريقة
ItemsCount
متوسط
خطأ
StdDev
متوسط
نسبة
RatioSD
ForEach
1000000
1.703 مللي ثانية
0.3010 مللي ثانية
0.8874 مللي ثانية
1.726 مللي ثانية
1.00
0.00
إلى
1000000
1.715 مللي ثانية
0.2859 مللي ثانية
0.8430 مللي ثانية
1.956 مللي ثانية
1.13
0.56

يسير ForEach عبر الصفيف بشكل أسرع بكثير من أجل . لماذا؟ لمعرفة ذلك ، نحتاج إلى مقارنة الكود بعد JIT:

مقارنة بين جميع خيارات foreach الثلاثة


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

الآن دعونا ننظر بعناية في ForEachWithLengthWIthoutLocalVariable. لا يوجد سوى جزء واحد غريب ، حيث يحدث sum + = length ليس للصفيف المتغير المحلي المحفوظ مسبقًا ، ولكن إلى جزء جديد يطلبه التطبيق من الذاكرة في كل مرة. هذا يعني أنه ستكون هناك طلبات ذاكرة N + 1 لطول الصفيف ، حيث N هو طول صفيف.

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

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

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


All Articles