لماذا لا يقوم Rust بتحسين الكود الذي يحتوي على مرجعين قابلين للتغيير؟ شرح للمبتدئين

مدرب بايثون أونلاين للمبتدئين

تعلم Python بسهولة دون إرهاق نظري. حل مهام عملية مع التحقق التلقائي واكتب الكود مباشرة في المتصفح.

ابدأ الدورة

مقدمة: لغز مترجم Rust



تخيل: أنت تكتب بلغة Rust، وأنت من أشد المعجبين بسلامة الذاكرة. أنت تعلم أن مرجعين قابلين للتغيير (&mut T) يشيران إلى نفس البيانات لا يمكن أن يتواجدا في نفس الوقت أبدًا. هذه إحدى القواعد الرئيسية للاقتراض (borrowing rules). يفرض مترجم Rust ذلك بصرامة.



قد يبدو الأمر منطقيًا، إذا كان المترجم متأكدًا من أن &mut لا يتداخلان (ليسا مستعارين)، فيجب عليه استخدام هذه المعرفة لتحسين الأداء بقوة. على سبيل المثال، إعادة ترتيب العمليات، تخزين القيم مؤقتًا في السجلات، وعدم إعادة قراءة الذاكرة. لكن المفارقة هي: مترجم Rust (LLVM) غالبًا لا يفعل ذلك. لماذا؟ فالأمر يبدو منطقيًا جدًا!



هذا السؤال حصد آلاف المشاهدات على Stack Overflow. واليوم، مثل المحققين الحقيقيين، سنحل هذا اللغز. لن نقرأ الإجابة فقط، بل سنفهم الأسباب العميقة، المرتبطة بإرث لغة C، و LLVM، وكيفية عمل التحسين. هيا بنا!



1. القاعدة الذهبية لـ Rust: مرجعان &mut يعنيان دائمًا بيانات مختلفة



لنتذكر الأساسيات. في Rust، هناك نوعان من المراجع:

  • غير قابل للتغيير (&T): يمكن أن يكون هناك عدد غير محدود منها، لكن لا يمكن تغيير البيانات.
  • قابل للتغيير (&mut T): يمكن أن يكون هناك مرجع واحد فقط، لكن يمكن تغيير البيانات.


هذه القاعدة هي noalias لـ &mut. يمكن للمترجم، من الناحية النظرية، أن يفترض: "إذا رأيت مرجعين &mut i32، فإنهما يشيران بالتأكيد إلى موقعين مختلفين في الذاكرة".



لكن دعنا ننظر إلى مثال بسيط يوضح المشكلة:



fn example(x: &mut i32, y: &mut i32) -> i32 {    *x = 42;    *y = 10;    *x // نتوقع 42، لكن...}


هنا الأمر واضح: x و y متغيران مختلفان. يضمن Rust أنهما لا يتداخلان. كان بإمكان المترجم ببساطة إرجاع 42 دون تنفيذ التعيينات. لكن هل هذا هو الحال بالفعل؟



2. المشكلة: LLVM و"عماه" تجاه ضمانات Rust



بطل قصتنا الرئيسي هو LLVM (Low Level Virtual Machine). يستخدم Rust LLVM كخلفية لتوليد كود الآلة. LLVM هو محسن قوي، لكنه لا يعرف قواعد Rust. إنه يعرف فقط قواعد C/C++.



في C/C++، هناك كلمة مفتاحية restrict (في C99)، والتي تخبر المترجم: "هذا المرجع فريد، ولا يشير أي شخص آخر إلى هذه البيانات". لكن في C/C++، restrict هو وعد من المبرمج، وليس ضمانًا من المترجم. إذا خالف المبرمج الوعد، فسيحدث سلوك غير محدد (undefined behavior).



أما Rust فيقدم ضمانًا قويًا جدًا. لكن لنقل هذا الضمان إلى LLVM، يجب على الواجهة الأمامية لـ Rust أن تضع علامة صريحة على &mut بسمة noalias في التمثيل الوسيط لـ LLVM (LLVM IR).



وهنا يبدأ الجزء المثير للاهتمام. يقوم Rust بوضع علامة noalias على &mut فقط في حالات معينة. وفي حالات أخرى، لا يفعل ذلك. لماذا؟



2.1. مشكلة تاريخية: UnsafeCell والكود غير الآمن



داخل Rust، يوجد نوع UnsafeCell<T>. وهو لبنة البناء لأنواع مثل Cell و RefCell و Mutex وغيرها، التي تسمح بالقابلية للتغيير الداخلي (interior mutability).



إذا كان لديك &mut UnsafeCell<i32>، فيمكنك من خلاله الحصول على مؤشر خام وتغيير البيانات حتى من خلال مرجع غير قابل للتغيير. هذا يكسر ضمان noalias!



لتجنب السلوك غير المحدد، لا يضع مترجم Rust علامة noalias على &mut إذا كان من المحتمل أن يكون وراءه UnsafeCell. لكن المشكلة أعمق من ذلك.



3. مثال واقعي: لماذا لا يعمل التحسين



لنعد إلى الدالة example

المدونات

توصيات الكتب