مفهرسي في C # تحت غطاء محرك السيارة: فهرسة أفضل من مؤشر داو جونز

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

صورة

المقاييس


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

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

العملية الصغرى (uop) هي عملية معينة تتكون منها كل تعليمات. يستخدم مفهوم العمليات الصغيرة للتحسينات مثل الدمج والتخزين المؤقت وإعادة الترتيب. لذلك ، على سبيل المثال ، تتكون تعليمة MOV من عملية واحدة صغيرة ، في حين أن تعليمات XCHG في سجالتين تتكون من 3 عمليات صغيرة (النهج من خلال "متغير مؤقت" ، أي سجل داخلي ، وذلك بفضل leotsarev للتحديث) ، تعليمة XCHG على السجل والذاكرة تتكون من 8 عمليات متناهية الصغر.

دمج العمليات الصغيرة (uops تنصهر) - كما ذكر أعلاه ، دمج العمليات الصغيرة هي واحدة من التحسينات. وهو يتكون من استبدال عمليتين صغيرتين بواحدة أكثر تعقيدًا.

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

الإنتاجية (الإنتاجية المتبادلة) - عدد دورات الساعة اللازمة لتنفيذ تعليمة واحدة ، شريطة أن يتم تنفيذ سلسلة من التعليمات المتماثلة وتعمل مع بيانات مستقلة.

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

هذه الأشكال مخصصة لبنية معالج Intel Skylake-X. هذا يتوافق مع معالج Intel Core i7-6700.

تجدر الإشارة إلى أن fastcall لأنظمة 64 بت يوفر نقل لا 2 ، ولكن 4 معلمات في السجلات (rcx ، rdx ، r8 ، r9).

مفهرسين بالأرقام


1. مفهرس صفيف


سننظر في الطرق التالية كمثال:

public int IndexerArray(int index, int[] array) { return array[index]; } 

اطلع على رمز لغة المجمّع لهذا المقتطف.

 1. cmp edx,dword ptr [r8+8] 2. jae 00007ff9`07288c78 3. movsxd rax,edx 4. mov eax,dword ptr [r8+rax*4+10h] 

يتحقق السطر الأول مما إذا كان الفهرس يتجاوز حدود المصفوفة. السطر الثاني يلقي استثناء إذا خرج. بعد ذلك ، نحسب موضع العنصر في الصفيف. الحقول الأولى في الصفيف هي معلومات الخدمة ، لذلك نحن بحاجة إلى تخطيها (10h = 16 بايت إضافية).

معلومات عن التعليمات:
عددتنصهر uopsمجموع uopsكمونالإنتاجية المتبادلة
11210.5
2111-2
31110.25
41120.5



2. قائمة المفضلة <>


رمز الحبر:
 public int IndexerList(int index, List<int> list) { return list[index]; } 


رمز لغة التجميع:

 1. cmp edx,dword ptr [r8+10h] 2. jae M00_L00 3. mov rax,qword ptr [r8+8] 4. cmp edx,dword ptr [rax+8] 5. jae 00007ff9`07268f56 6. movsxd rdx,edx 7. mov eax,dword ptr [rax+rdx*4+10h] ret M00_L00 call System.ThrowHelper.ThrowArgumentOutOfRange_IndexException() 

هناك بوضوح المزيد من التعليمات هنا. من الواضح أن مفهرس الورقة يلف مفهرس الصفيف. النقطة المهمة هي أن التحقق من تجاوز حدود المصفوفة يتم مرتين. لذلك ، يتحقق التعليمة الأولى مما إذا كان الفهرس يتجاوز حدود الورقة. إذا كان الأمر كذلك ، فسنقفز (التعليمة 2) إلى مكالمة واضحة جدًا ، مع استثناء إذا تجاوزت حدود المصفوفة. يستخدم فحص الحدود هذا الحقل الداخلي للورقة ، وهو الثاني بالترتيب (إزاحة 10 h (16) بايت من بداية النوع ، 8 إلى المؤشر إلى جدول الطريقة و 8 إلى رابط المصفوفة الداخلية - الحقل الأول). في السطر الثالث ، وضعنا في rax تسجيل عنوان الصفيف الداخلي - الحقل الأول (عن طريق القياس ، إزاحة 8 بايت هو مؤشر إلى جدول الطريقة). يتبع ذلك القسم المألوف بالفعل - مرجع فهرس الصفيف (الأسطر 4-7). هنا ، للتحقق من الحدود ، يتم استخدام الحقل الداخلي للصفيف.
لقد حاولت إزالة الأشياء التي لا تتعلق بشكل مباشر بالفهرسة ، ولكن من الجدير بمكان ترك الإجازات حتى لا يبدو أنه في نهاية كل مكالمة لعنصر الورقة سيكون هناك استثناء: D

بالمناسبة ، حتى لا تعذبك بالمضاربة ، يرجى التعرف على تنفيذ الورقة بالرجوع إليها . يتم استخدام casting to intts غير الموقعة لتقليل عدد المقارنات.

نتيجة لذلك ، حصلنا على 7 إرشادات للوصول إلى الفهرس بنجاح ، وهو 3 أكثر من المصفوفة.

معلومات عن التعليمات:
عددتنصهر uopsمجموع uopsكمونالإنتاجية المتبادلة
11210.5
2111-2
31120.5
41210.5
5111-2
61110.25
71120.5


جديد - سبان <>


الجمع:

 public int IndexerSpan(int index, Span<int> span) { return span[index]; } 

ولغة التجميع:

 1. cmp edx,dword ptr [r8+8] 2. jae 00007ff9`07278f69 3. mov rax,qword ptr [r8] 4. movsxd rdx,edx 5. mov eax,dword ptr [rax+rdx*4] 

عندما تم الإعلان عن الامتدادات ، وعدونا بأنهم صنعوا بحكمة ، بدعم من وقت التشغيل. ولم يكذبوا ما يقولون. في الواقع ، يختلف عن المصفوفة الكلاسيكية في تعليمة واحدة فقط ، وهي خطوة إضافية للوصول إلى العنوان. استنادا إلى هذا الرمز ، يكون عنوان موقع الذاكرة مخفيًا داخل النطاق ، حيث توجد العناصر ، والتي نحصل عليها في السطر 3. يمكن أن يكون هذا عنوانًا لمكان محدد في صفيف أو سطر أو جزء من الذاكرة على المكدس.
انقر هنا للحصول على مقدمة لمفهرس سبان للمتعة. قد تلاحظ أن هناك تطبيقين مختلفين ، حسب متغير البيئة. PROJECTN هو الاسم الرمزي للإصدار الأول من .NET Native لـ UWP. لذلك ، نحن مهتمون أكثر بالإصدار الثاني من المفهرس. انها الموسومة [جوهري] . علاوة على ذلك ، إذا نظرت إلى الفصل الثابت غير الآمن المستخدم في تنفيذ هذا المفهرس ، يمكنك العثور على معلومات تمثل تطبيقات معظم الطرق في هذا الملف على أنها مضمّنة.

دعوات الأسلوب أو المراجع إلى الحقول التي تحمل السمة [الداخلية] لها دعم من وقت التشغيل.

في CoreCLR ، يتم استبدال هياكل هذه الطرق بواسطة EE (محرك التنفيذ) برمز غير آمن (غير آمن). إذا كنت بحاجة إلى مزيد من التفاصيل ، يمكنك البدء في الحفر باستخدام طريقة getILIntrinsicImplementationForUnsafe .

معلومات حول كيفية عمل هذا في CoreRT (التي تهمني قليلاً) ،
يمكنك البدء في النظر إلى Internal.IL.Stubs.UnsafeIntrinsics .

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

معلومات عن التعليمات:
عددتنصهر uopsمجموع uopsكمونالإنتاجية المتبادلة
11210.5
2111-2
31120.5
41110.25
51120.5


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

الآن ، دعونا نحاول إلقاء نظرة على الطرق المختلفة التي يمكننا من خلالها تحديد صفيف ثنائي الأبعاد: صفيف من المصفوفات (صفيف خشنة) ومصفوفة متعددة الأبعاد (صفيف متعدد الأبعاد).

4. مجموعة متعددة الأبعاد


كود حاد:

 public int IndexerDimensionalArray(int index1, int index2, int[,] demensionalArray) { return demensionalArray[index1, index2]; } 

لغة التجميع:

 1. mov eax,edx 2. sub eax,dword ptr [r9+18h] 3. cmp eax,dword ptr [r9+10h] 4. jae 00007ff9`00098fe6 5. mov edx,r8d 6. sub edx,dword ptr [r9+1Ch] 7. cmp edx,dword ptr [r9+14h] 8. jae 00007ff9`00098fe6 9. mov ecx,dword ptr [r9+14h] 10. imul rcx,rax 11. mov rax,rdx 12. add rax,rcx 13. mov eax,dword ptr [r9+rax*4+20h] 

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

معلومات عن التعليمات:
عددتنصهر uopsمجموع uopsكمونالإنتاجية المتبادلة
1110-10.25
2120.5
31210.5
4111-2
5110-10.25
6120.5
71210.5
8111-2
91120.5
101131
11110-10.25
121110.25
131120.5



5. مجموعة من المصفوفات (مجموعة خشنة)


 public int IndexerJaggedArray(int index, int index2, int[][] jaggedArray) { return jaggedArray[index][index2]; } 

المجمع:

 1. cmp edx,dword ptr [r9+8] 2. jae 00007ff9`00098f95 3. movsxd rax,edx 4. mov rax,qword ptr [r9+rax*8+10h] 5. cmp r8d,dword ptr [rax+8] 6. jae 00007ff9`00098f95 7. movsxd rdx,r8d 8. mov eax,dword ptr [rax+rdx*4+10h] 

والأكثر إثارة للاهتمام - لدينا تعليمات أقل من النوع الذي صنع خصيصًا من أجل الأبعاد المتعددة.

معلومات عن التعليمات:
عددتنصهر uopsمجموع uopsكمونالإنتاجية المتبادلة
11210.5
2111-2
31110.25
41120.5
51210.5
6111-2
71110.25
81120.5


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

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

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

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

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


All Articles