المرجع المحلي والعودة المرجع في C #: مخاطر الأداء

منذ البداية ، دعم C # تمرير الوسائط حسب القيمة أو المرجع. ولكن قبل الإصدار 7 ، كان المحول البرمجي C # يدعم طريقة واحدة فقط لإرجاع قيمة من طريقة (أو خاصية) - الإرجاع حسب القيمة. في C # 7 ، تغير الوضع مع إدخال ميزتين جديدتين: مرجع العودة ومرجع السكان المحليين. المزيد عنهم وأدائهم - تحت الخفض.



الأسباب


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

لإثبات ذلك ، سيتعين علينا اللجوء إلى الطريقة المحظورة - استخدم نوع القيمة القابلة للتغيير:

public struct Mutable { private int _x; public Mutable(int x) => _x = x; public int X => _x; public void IncrementX() { _x++; } } [Test] public void CheckMutability() { var ma = new[] {new Mutable(1)}; ma[0].IncrementX(); // X has been changed! Assert.That(ma[0].X, Is.EqualTo(2)); var ml = new List<Mutable> {new Mutable(1)}; ml[0].IncrementX(); // X hasn't been changed! Assert.That(ml[0].X, Is.EqualTo(1)); } 

سينجح الاختبار لأن مفهرس الصفيف يختلف اختلافًا كبيرًا عن مفهرس القائمة.

يعطي المترجم C # تعليمات خاصة لمفهرس المصفوفة - ldelema ، والتي تُرجع ارتباطًا مُدارًا لعنصر من هذه المصفوفة. بشكل أساسي ، يقوم مفهرس الصفيف بإرجاع عنصر حسب المرجع. ومع ذلك ، لا يمكن أن تتصرف List بنفس الطريقة ، لأنه في C # لم يكن من الممكن * إرجاع اسم مستعار للحالة الداخلية. لذلك ، يقوم مفهرس القائمة بإرجاع عنصر حسب القيمة ، أي إرجاع نسخة من هذا العنصر.

* كما سنرى قريبًا ، لا يزال مفهرس القائمة لا يمكنه إرجاع عنصر حسب المرجع.

هذا يعني أن ma [0] .IncrementX () يستدعي الطريقة التي تعدل العنصر الأول للصفيف ، بينما ml [0] .IncrementX () يستدعي الطريقة التي تعدل نسخة العنصر دون التأثير على القائمة الأصلية.

قيم الإرجاع والمتغيرات المحلية المرجعية: الأساسيات


معنى هذه الوظائف بسيط للغاية: يسمح لك الإعلان عن القيمة المرجعية التي تم إرجاعها بإرجاع الاسم المستعار لمتغير موجود ، ويمكن للمتغير المحلي المرجعي تخزين هذا الاسم المستعار.

1. مثال بسيط:

 [Test] public void RefLocalsAndRefReturnsBasics() { int[] array = { 1, 2 }; // Capture an alias to the first element into a local ref int first = ref array[0]; first = 42; Assert.That(array[0], Is.EqualTo(42)); // Local function that returns the first element by ref ref int GetByRef(int[] a) => ref a[0]; // Weird syntax: the result of a function call is assignable GetByRef(array) = -1; Assert.That(array[0], Is.EqualTo(-1)); } 

2. إرجاع القيم المرجعية ومعدل القراءة فقط

يمكن للقيمة المرجعية التي تم إرجاعها إرجاع الاسم المستعار لحقل المثيل ، وبدءًا من C # الإصدار 7.2 ، يمكنك إرجاع الاسم المستعار دون أن تتمكن من الكتابة إلى الكائن المقابل باستخدام معدِّل المرجع للقراءة فقط:

 class EncapsulationWentWrong { private readonly Guid _guid; private int _x; public EncapsulationWentWrong(int x) => _x = x; // Return an alias to the private field. No encapsulation any more. public ref int X => ref _x; // Return a readonly alias to the private field. public ref readonly Guid Guid => ref _guid; } [Test] public void NoEncapsulation() { var instance = new EncapsulationWentWrong(42); instance.X++; Assert.That(instance.X, Is.EqualTo(43)); // Cannot assign to property 'EncapsulationWentWrong.Guid' because it is a readonly variable // instance.Guid = Guid.Empty; } 

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

3. القيود القائمة. يمكن أن يكون إرجاع اسم مستعار خطرًا: سيؤدي استخدام اسم مستعار لمتغير يتم وضعه على المكدس بعد اكتمال الطريقة إلى تعطل التطبيق. لجعل هذه الوظيفة آمنة ، يطبق المترجم C # قيود مختلفة:

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

لمزيد من المعلومات ، نوصيك بالاطلاع على المنشور الممتاز لقواعد الإرجاع الآمنة لإرجاع المرجع . مؤلف المقالة ، فلاديمير سادوف ، هو منشئ دالة مرجع الرجوع لمترجم C #.

الآن بعد أن أصبح لدينا فكرة عامة عن القيم المرجعية التي تم إرجاعها والمتغيرات المحلية المرجعية ، دعنا نلقي نظرة على كيفية استخدامها.

استخدام القيم المرجعية التي تم إرجاعها في الفهارس


لاختبار تأثير هذه الوظائف على الأداء ، سنقوم بإنشاء مجموعة فريدة غير قابلة للتغيير تسمى NaiveImmutableList <T> ومقارنتها مع T [] وقائمة للهياكل ذات الأحجام المختلفة (4 و 16 و 32 و 48).

 public class NaiveImmutableList<T> { private readonly int _length; private readonly T[] _data; public NaiveImmutableList(params T[] data) => (_data, _length) = (data, data.Length); public ref readonly T this[int idx] // R# 2017.3.2 is completely confused with this syntax! // => ref (idx >= _length ? ref Throw() : ref _data[idx]); { get { // Extracting 'throw' statement into a different // method helps the jitter to inline a property access. if ((uint)idx >= (uint)_length) ThrowIndexOutOfRangeException(); return ref _data[idx]; } } private static void ThrowIndexOutOfRangeException() => throw new IndexOutOfRangeException(); } struct LargeStruct_48 { public int N { get; } private readonly long l1, l2, l3, l4, l5; public LargeStruct_48(int n) : this() => N = n; } // Other structs like LargeStruct_16, LargeStruct_32 etc 

يتم إجراء اختبار الأداء لجميع المجموعات ويضيف كل قيم الخاصية N لكل عنصر:

 private const int elementsCount = 100_000; private static LargeStruct_48[] CreateArray_48() => Enumerable.Range(1, elementsCount).Select(v => new LargeStruct_48(v)).ToArray(); private readonly LargeStruct_48[] _array48 = CreateArray_48(); [BenchmarkCategory("BigStruct_48")] [Benchmark(Baseline = true)] public int TestArray_48() { int result = 0; // Using elementsCound but not array.Length to force the bounds check // on each iteration. for (int i = 0; i < elementsCount; i++) { result = _array48[i].N; } return result; } 

النتائج هي كما يلي:



على ما يبدو ، هناك خطأ ما! أداء مجموعة NaiveImmutableList <T> الخاصة بنا هو نفسه القائمة. ماذا حدث؟

إرجاع القيم مع معدّل للقراءة فقط: كيف يعمل


كما ترى ، تقوم مفهرس NaiveImmutableList <T> بإرجاع ارتباط للقراءة فقط باستخدام معدّل المرجع للقراءة فقط. هذا مبرر تمامًا ، لأننا نريد الحد من قدرة العملاء على تغيير الحالة الأساسية لمجموعة ثابتة. ومع ذلك ، فإن الهياكل التي نستخدمها في اختبار الأداء ليست قابلة للقراءة فقط.

سيساعدنا هذا الاختبار على فهم السلوك الأساسي:

 [Test] public void CheckMutabilityForNaiveImmutableList() { var ml = new NaiveImmutableList<Mutable>(new Mutable(1)); ml[0].IncrementX(); // X has been changed, right? Assert.That(ml[0].X, Is.EqualTo(2)); } 

فشل الاختبار! لكن لماذا؟ لأن بنية "روابط للقراءة فقط" تشبه بنية في المعدلات والمجالات للقراءة فقط فيما يتعلق بالبنى: يولد المترجم نسخة واقية في كل مرة يتم فيها استخدام عنصر هيكل. هذا يعني أن مل [0]. لا يزال يُنشئ نسخة من العنصر الأول ، لكن المفهرس لا يقوم بذلك: يتم إنشاء النسخة عند نقطة الاستدعاء.

هذا السلوك منطقي بالفعل. يدعم المترجم C # تمرير الوسيطات بالقيمة والمرجع وبواسطة "رابط للقراءة فقط" باستخدام المعدل في (لمزيد من التفاصيل ، انظر في المعدّل والبنيات للقراءة فقط في C # ("في المعدِّل والبنى للقراءة فقط في C # ")). يدعم المترجم الآن ثلاث طرق مختلفة لإرجاع قيمة من طريقة: حسب القيمة ، حسب المرجع ، ورابط للقراءة فقط.

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

 private int _n; public ref readonly int ByReadonlyRef() => ref _n; 

في هذه الحالة ، يتم تجميع طريقة ByReadonlyRef بكفاءة في:

 [InAttribute] [return: IsReadOnly] public int* ByReadonlyRef() { return ref this._n; } 

التشابه بين المعدل في الرابط والقراءة فقط يعني أن هذه الوظائف ليست مناسبة جدًا للهياكل العادية ويمكن أن تسبب مشاكل في الأداء. فكر في مثال:

 public struct BigStruct { // Other fields public int X { get; } public int Y { get; } } private BigStruct _bigStruct; public ref readonly BigStruct GetBigStructByRef() => ref _bigStruct; ref readonly var bigStruct = ref GetBigStructByRef(); int result = bigStruct.X + bigStruct.Y; 

إلى جانب بناء الجملة غير المعتاد عند الإعلان عن متغير لـ bigStruct ، يبدو الرمز جيدًا. الهدف واضح: ترجع BigStruct حسب المرجع لأسباب تتعلق بالأداء. لسوء الحظ ، نظرًا لأن بنية BigStruct قابلة للكتابة ، يتم إنشاء نسخة واقية في كل مرة يتم فيها الوصول إلى العنصر.

استخدام القيم المرجعية التي تم إرجاعها في الفهارس. المحاولة رقم 2


لنجرب نفس مجموعة الاختبارات للهياكل للقراءة فقط بأحجام مختلفة:



الآن النتائج أكثر منطقية. لا يزال وقت المعالجة يتزايد للهياكل الكبيرة ، ولكن هذا متوقع ، لأن معالجة أكثر من 100 ألف من الهياكل الأكبر تستغرق وقتًا أطول. ولكن الآن وقت التشغيل لـ NaiveimmutableList <T> قريب جدًا من الوقت T [] وأفضل بكثير من حالة List.

الخلاصة


  • يجب التعامل مع القيم المرجعية التي تم إرجاعها بعناية لأنها يمكن أن تكسر التغليف.
  • تكون القيم المرجعية التي تم إرجاعها مع مُعدِّل للقراءة فقط فعالة للهياكل للقراءة فقط. في حالة الهياكل التقليدية ، قد تحدث مشاكل في الأداء.
  • عند العمل باستخدام الهياكل القابلة للكتابة ، تقوم القيم المرجعية التي تم إرجاعها باستخدام معدّل القراءة فقط بإنشاء نسخة واقية في كل مرة يتم فيها استخدام المتغير ، مما قد يتسبب في مشاكل في الأداء.

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

ستظهر روابط PS للقراءة فقط في BCL. تم عرض طرق المرجع للقراءة فقط للوصول إلى العناصر في مجموعات ثابتة في الطلب التالي لدمج التغييرات في corefx repo ( تنفيذ ItemRef API Proposal ("اقتراح لتضمين ItemRef API")). لذلك ، من المهم جدًا أن يفهم الجميع ميزات استخدام هذه الوظائف وكيف ومتى يجب تطبيقها.

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


All Articles