اذهب إلى المحتوى

الاختيار بين الحلقات Loops والمكررات Iterators في لغة رست


Naser Dakhel

تعرفنا في الفصل السابق "معالجة سلسلة من العناصر باستخدام المكررات iterators" والذي يليه "استخدام المكررات Iterators في تطبيق سطر أوامر" على المكررات Iterators وكيفية استخدامها عمليًا ولعل السؤال الذي خطر ببالك الآن بعد قراءتهما هو: أيّ طرق التطبيق ينبغي عليك استعمالها عند برمجتك لبرنامجك ولماذا؟ التطبيق الأصلي في الشيفرة 21، أم الإصدار باستخدام المكررات في الشيفرة 22 مقال "استخدام المكررات Iterators في تطبيق سطر أوامر بلغة رست"؟ يفضِّل معظم مبرمجي لغة رست استخدام المكررات، وعلى الرغم من أن هذه الطريقة أصعب فهمًا في البداية إلا أنك ستفضلها بعد الاعتياد عليها وعلى محولات المكررات المختلفة، إذ ستركّز عندها شيفرتك البرمجية على الهدف العام للحلقة بدلًا من إضاعة الوقت في ضبط الأجزاء المختلفة من الحلقات وإنشاء أشعة جديدة. تخلّصنا هذه الطريقة من الكثير من الشيفرات البرمجية الاعتيادية بحيث يكون من الأسهل رؤية المقصد الأساسي من الشيفرة البرمجية على نحوٍ فريد لكل استخدام، مثل ترشيح كل عنصر في المكرر بحسب شرط ما.

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

المقارنة بين أداء الحلقات والمكررات

عليك معرفة أيّ التطبيقين أسرع، الحلقات أم المكررات؟ وذلك لتحديد متى يجب عليك استخدام أحد التطبيقين؛ هل إصدار الدالة search (من المقال السابق) باستخدام حلقة for أسرع أم إصدار المكررات؟

ننفّذ اختبار أداء بكتابة كامل محتويات رواية "مغامرات شيرلوك هولمز The Adventures of Sherlock Holmes" لكاتبها سير آرثر كونان دويل Sir Arthur Conan Doyle إلى String والبحث عن الكلمة "the" في المحتويات. إليك نتائج اختبار الأداء على كلا الإصدارين لدالة search باستخدام حلقة for، وباستخدام المكرّرات:

test bench_search_for  ... bench:  19,620,300 ns/iter (+/- 915,700)
test bench_search_iter ... bench:  19,234,900 ns/iter (+/- 657,200)

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

ينبغي عليك التحقق من نصوص متفاوتة الطول للوسيط contents للحصول على اختبار أداء أكثر وضوًحا، واستخدام كلمات مختلفة من أطوال متفاوتة للوسيط query وتجربة مختلف أنواع الحالات. ما نريد الوصول إليه هنا هو التالي: على الرغم من أن المكررات تستخدم تطبيقًا مجرّدًا عالي المستوى، إلا أنها تُصرَّف إلى شيفرة برمجية مماثلة لشيفرة تطبيق منخفض المستوى كتبتها بنفسك، إذ أن المكررات هي من الطرق المجرّدة عديمة الحمل zero-cost abstraction في رست، وهذا يعني أن استخدام التجريد لن يؤثر على وقت التشغيل. هذا الأمر مماثل لكيفية تعريف بيارن ستروستروب Bjarne Stroustrup مصمّم لغة سي بلس بلس C++‎ ومطبّقها لمبدأ انعدام الحمل غير المباشر zero-overhead في كتابه "أساسيات سي بلس بلس Foundations of C++‎"‏ (2012):

اقتباس

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

الشيفرة البرمجية التالية هي مثال آخر مأخوذ من برنامج فك ترميز صوت، إذ تستخدم خوارزمية فك الترميز عملية التوقع الخطي الرياضي لتوقّع القيم المستقبلية بناءً على دالة خطية تأخذ العينات السابقة. تستخدم هذه الشيفرة البرمجية سلسلة مكررات لإنجاز بعض العمليات الرياضية على ثلاثة متغيرات موجودة في النطاق scope، هي: buffer شريحة البيانات، ومصفوفة من 12 عنصر coefficients وأي مقدار إزاحة بالبيانات مُخزَّن في qlb_shift، وقد صرّحنا عن هذه المتغيرات داخل المثال دون إسناد أي قيمة لها. على الرغم من أن هذه الشيفرة البرمجية ليست مفيدة خارج سياقها إلا أنها مثال مختصر وواقعي على كيفية ترجمة رست للأفكار والتطبيقات عالية المستوى إلى مستوى منخفض.

let buffer: &mut [i32];
let coefficients: [i64; 12];
let qlp_shift: i16;

for i in 12..buffer.len() {
    let prediction = coefficients.iter()
                                 .zip(&buffer[i - 12..i])
                                 .map(|(&c, &s)| c * s as i64)
                                 .sum::<i64>() >> qlp_shift;
    let delta = buffer[i];
    buffer[i] = prediction as i32 + delta;
}

تمرّ الشيفرة البرمجية على القيم الاثنا عشر الموجودة في coefficients وتستخدم التابع zip لاقتران القيم الاثنا عشر الحالية مع القيم الاثني العشر السابقة الموجودة في buffer وذلك لحساب قيمة التوقع prediction، ثم نضرب قيمة كل زوج على حدى ونجمع النتيجة ونُزيح shift البتات bits في نتيجة الجمع بمقدار qlb_shift بت إلى اليمين.

تركّز العمليات الحسابية في تطبيقات مثل فك ترميز الصوت على الأداء في المقام الأول. نُنشئ هنا مكررًا باستخدام محوّلَين adaptor ومن ثم نستهلك القيمة. ما هي شيفرة أسيمبلي Assembly التي ستُصرَّف إليها شيفرة رست هذه؟ تُصرَّف حتى الوقت الحالي إلى نفس شيفرة أسيمبلي التي قد تكتبها بنفسك. ليس هناك أي حلقة للمكرر الخاص بالقيم coefficients، إذ تعرف رست أن هناك 12 تكرارًا فقط، لذا فهي تنشر الحلقة؛ وعملية النشر unrolling هي عملية لتحسين الأداء، إذ يُزال فيها الحمل الإضافي غير المباشر الخاص بالشيفرة البرمجية المُتحكمة بالحلقة وتُولَّد شيفرة برمجية متكررة بدلًا من ذلك للشيفرة البرمجية في كل تكرار من الحلقة.

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

ترجمة -وبتصرف- لقسم من الفصل Functional Language Features: Iterators and Closures من كتاب The Rust Programming Language.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...