التوازي على مستوى الحلقة

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

وصف

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

مثال

ضع في اعتبارك الكود التالي الذي يعمل على قائمة l، حيث l.size() == n.

for ( int i = 0 ; i < n ; ++ i ) { l [ i ] += 10 ; // s1 }

تأخذ كل دورة من حلقات التكرار القيمة من الفهرس الحالي لـ l، وتزيدها بمقدار 10. إذا s1استغرقت العبارة Tوقتًا للتنفيذ، فإن الحلقة تستغرق وقتًا n * Tللتنفيذ بالتسلسل، متجاهلةً الوقت الذي تستغرقه بنيات الحلقة. الآن، لنفترض نظامًا بمعالجات pحيث p > n. إذا nعملت الخيوط بالتوازي، فإن وقت تنفيذ جميع nالخطوات ينخفض ​​إلى T.

تُنتج الحالات الأقل بساطة نتائج غير متسقة، أي غير قابلة للتسلسل . لنفترض وجود حلقة تكرارية تعمل على نفس القائمة l.

for ( int i = 0 ; i < n ; ++ i ) { l [ i ] = l [ i - 1 ] + 10 ; // s1 }

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

التبعيات في الكود

توجد عدة أنواع من التبعيات التي يمكن العثور عليها داخل الكود. [ 1 ] [ 2 ]

يكتبالترميزوصف
الاعتماد الحقيقي (للتدفق)s1 ->T s2يعني التبعية الحقيقية بين s1 و s2 أن s1 يكتب إلى موقع يقرأ منه s2 لاحقًا
مكافحة الإدمانs1 ->A s2يعني عدم الاعتماد المتبادل بين s1 و s2 أن s1 يقرأ من موقع يتم الكتابة إليه لاحقًا بواسطة s2.
الاعتماد على المخرجاتs1 ->O s2يعني وجود تبعية إخراج بين s1 و s2 أن s1 و s2 يكتبان في نفس الموقع.
الاعتماد على المدخلاتs1 ->I s2يعني وجود اعتماد إدخال بين s1 و s2 أن s1 و s2 يقرأان من نفس الموقع.

للحفاظ على السلوك التسلسلي للحلقة عند تشغيلها بالتوازي، يجب الحفاظ على التبعية الحقيقية. ويمكن معالجة التبعية المضادة وتبعية الإخراج بمنح كل عملية نسخة خاصة بها من المتغيرات (المعروفة باسم التخصيص). [ 1 ]

مثال على التبعية الحقيقية

كثافة العمليات أ ، ب ؛ // s1 أ = 2 ; // s2 ب = أ + 40 ؛ // S3

s2 ->T s3، مما يعني أن s2 لديه اعتماد حقيقي على s3 لأن s2 يكتب إلى المتغير a، الذي يقرأ منه s3.

مثال على مكافحة الاعتماد

كثافة العمليات أ ، ب = 40 ؛ // s1 أ = ب - 38 ؛ // s2 ب = -1 ; // S3

s2 ->A s3، مما يعني أن s2 لديه اعتماد عكسي على s3 لأن s2 يقرأ من المتغير bقبل أن يكتب s3 إليه.

مثال على اعتماد المخرجات

كثافة العمليات أ ، ب = 40 ؛ // s1 أ = ب - 38 ؛ // s2 أ = 2 ; // S3

s2 ->O s3، مما يعني أن s2 يعتمد في مخرجاته على s3 لأن كلاهما يكتب إلى المتغير a.

مثال على الاعتماد على المدخلات

int a , b , c = 2 ; // s1 a = c - 1 ; // s2 b = c + 1 ; // s3

s2 ->I s3، مما يعني أن s2 يعتمد على s3 في الإدخال لأن s2 و s3 كلاهما يقرأ من المتغير c.

الاعتماد في الحلقات

الاعتمادية المنقولة عبر الحلقة مقابل الاعتمادية المستقلة عن الحلقة

يمكن أن تحتوي الحلقات على نوعين من التبعية:

  • الاعتماد الذي تحمله الحلقة
  • التبعية المستقلة عن الحلقة

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

في مثال الكود التالي المستخدم لتبديل قيم مصفوفتين بطول n، هناك اعتماد مستقل عن الحلقة لـ s1 ->T s3.

for ( int i = 1 ; i < n ; ++ i ) { tmp = a [ i ]; // s1 a [ i ] = b [ i ]; // s2 b [ i ] = tmp ; // s3 }

في التبعية المُحملة في الحلقة، تعتمد التعليمات في تكرار حلقة ما على التعليمات في تكرار آخر من نفس الحلقة. وتستخدم التبعية المُحملة في الحلقة نسخة مُعدلة من صيغة التبعية التي تم شرحها سابقًا.

مثال على التبعية المنقولة في الحلقة حيث يشير s1[i] ->T s1[i + 1]، حيث iيشير إلى التكرار الحالي، i + 1ويشير إلى التكرار التالي.

for ( int i = 1 ; i < n ; ++ i ) { a [ i ] = a [ i - 1 ] + 1 ; // s1 }

رسم بياني لاعتمادية الحلقة المحمولة

يُظهر الرسم البياني للاعتمادات المنقولة عبر الحلقة الاعتمادات المنقولة بين التكرارات. يُدرج كل تكرار كعقدة على الرسم البياني، وتُظهر الحواف الموجهة الاعتمادات الحقيقية والمضادة واعتمادات المخرجات بين كل تكرار.

الأنواع

توجد مجموعة متنوعة من المنهجيات لتنفيذ الحلقات بالتوازي.

  • حلقة موزعة
  • التوازي DOALL
  • التوازي عبر DOACROSS
  • الحلزون [ 3 ]
  • التوازي في DOPIPE

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

يمكن تقسيم عملية موازاة برنامج تسلسلي إلى الخطوات المنفصلة التالية. [ 1 ] كل عملية موازاة حلقية محددة أدناه تقوم بتنفيذها ضمنيًا.

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

حلقة موزعة

عندما تحتوي حلقة تكرارية على تبعية مُرتبطة، فإن إحدى طرق موازاتها هي توزيع الحلقة على عدة حلقات تكرارية منفصلة. تُفصل العبارات غير المُعتمدة على بعضها البعض بحيث يمكن تنفيذ هذه الحلقات المُوزعة بالتوازي. على سبيل المثال، انظر إلى الكود التالي.

for ( int i = 1 ; i < n ; ++ i ) { a [ i ] = a [ i - 1 ] + b [ i ]; // s1 c [ i ] += d [ i ]; // s2 }

تحتوي الحلقة على تبعية مرتبطة بالحلقة، s1[i] ->T s1[i + 1]لكن s2 و s1 لا تحتويان على تبعية مستقلة عن الحلقة، لذلك يمكننا إعادة كتابة الكود على النحو التالي.

// loop1 for ( int i = 1 ; i < n ; ++ i ) { a [ i ] = a [ i - 1 ] + b [ i ]; // s1 }// الحلقة الثانية for ( int i = 1 ; i < n ; ++ i ) { c [ i ] += d [ i ]; // s2 }

لاحظ أنه يمكن الآن تنفيذ الحلقتين loop1 و loop2 بالتوازي. فبدلاً من تنفيذ تعليمة واحدة بالتوازي على بيانات مختلفة كما هو الحال في التوازي على مستوى البيانات، تقوم الحلقات المختلفة هنا بتنفيذ مهام مختلفة على بيانات مختلفة. لنفترض أن زمن تنفيذ s1 و s2 هوتيs1{\displaystyle T_{s_{1}}}وتيs2{\displaystyle T_{s_{2}}}إذن، يكون وقت تنفيذ الشكل التسلسلي للكود أعلاه هون(تيs1+تيs2){\displaystyle n\cdot (T_{s_{1}}+T_{s_{2}})}والآن، بما أننا قمنا بتقسيم العبارتين ووضعناهما في حلقتين مختلفتين، فإن ذلك يعطينا وقت تنفيذ قدرهنتيs1+تيs2{\displaystyle n\cdot T_{s_{1}}+T_{s_{2}}}نطلق على هذا النوع من التوازي اسم التوازي الوظيفي أو التوازي المتعلق بالمهام.

التوازي DOALL

يتحقق التوازي DOALL عندما يمكن تنفيذ التعليمات داخل حلقة تكرارية بشكل مستقل (أي عندما لا توجد تبعية ناتجة عن الحلقة). [ 1 ] على سبيل المثال، لا يقرأ الكود التالي من المصفوفة a، ولا يُحدّث المصفوفات b, c. لا توجد أي تكرارات تعتمد على أي تكرار آخر.

for ( int i = 0 ; i < n ; ++ i ) { a [ i ] = b [ i ] + c [ i ]; // s1 }

لنفترض أن زمن تنفيذ واحد لـ s1 هوتيs1{\displaystyle T_{s_{1}}}إذن، يكون وقت تنفيذ الشكل التسلسلي للكود أعلاه هونتيs1{\displaystyle n\cdot T_{s_{1}}}وبما أن التوازي في تنفيذ جميع العمليات (DOALL) موجود عندما تكون جميع التكرارات مستقلة، فإنه يمكن تحقيق تسريع من خلال تنفيذ جميع التكرارات بالتوازي، مما يعطينا وقت تنفيذ قدرهتيs1{\displaystyle T_{s_{1}}}، وهو الوقت المستغرق لتكرار واحد في التنفيذ التسلسلي.

يوضح المثال التالي، باستخدام رمز زائف مبسط، كيف يمكن موازاة حلقة لتنفيذ كل تكرار بشكل مستقل.

beginParallelism ();for ( int i = 0 ; i < n ; ++ i ) { a [ i ] = b [ i ] + c [ i ]; // s1 endParallelism (); }حاجز ()؛

التوازي عبر DOACROSS

توجد التوازية في DOACROSS حيث يتم تنفيذ تكرارات حلقة ما بالتوازي عن طريق استخراج العمليات الحسابية التي يمكن إجراؤها بشكل مستقل وتشغيلها في وقت واحد. [ 5 ]

توجد خاصية التزامن لفرض التبعية التي تحملها الحلقة.

ضع في اعتبارك الحلقة المتزامنة التالية مع التبعية s1[i] ->T s1[i + 1].

for ( int i = 1 ; i < n ; ++ i ) { a [ i ] = a [ i - 1 ] + b [ i ] + 1 ; }

تُنفذ كل دورة تكرارية إجراءين

  • احسبa[i - 1] + b[i] + 1
  • قم بتعيين القيمة إلىa[i]

يمكن تقسيم عملية حساب القيمة a[i - 1] + b[i] + 1، ثم إجراء عملية الإسناد، إلى سطرين (العبارتين s1 و s2):

int tmp = b [ i ] + 1 ; // s1 a [ i ] = a [ i - 1 ] + tmp ; // s2

السطر الأول، int tmp = b[i] + 1;، لا يعتمد على حلقة التكرار. يمكن بعد ذلك تنفيذ الحلقة بالتوازي عن طريق حساب القيمة المؤقتة بالتوازي، ثم مزامنة عملية الإسناد إلى a[i].

post ( 0 ); for ( int i = 1 ; i < n ; ++ i ) { int tmp = b [ i ] + 1 ; // s1 wait ( i - 1 );a [ i ] = a [ i - 1 ] + tmp ; // s2 post ( i ); }

لنفترض أن وقت تنفيذ s1 و s2 هوتيs1{\displaystyle T_{s_{1}}}وتيs2{\displaystyle T_{s_{2}}}إذن، يكون وقت تنفيذ الشكل التسلسلي للكود أعلاه هون(تيs1+تيs2){\displaystyle n\cdot (T_{s_{1}}+T_{s_{2}})}والآن، بفضل وجود التوازي في DOACROSS، يمكن تحقيق تسريع من خلال تنفيذ التكرارات بطريقة متسلسلة، مما يمنحنا وقت تنفيذ قدرهتيs1+نتيs2{\displaystyle T_{s_{1}}+n\cdot T_{s_{2}}}.

التوازي DOPIPE

تُطبّق خوارزمية DOPIPE التوازي المُتدرّج لمعالجة التبعية المُتوارثة في الحلقات، حيث يتم توزيع تكرار الحلقة على حلقات مُتزامنة مُتعددة. [ 1 ] يهدف DOPIPE إلى العمل كخط تجميع، حيث تبدأ كل مرحلة بمجرد توفر بيانات كافية لها من المرحلة السابقة. [ 6 ]

ضع في اعتبارك الكود المتزامن التالي مع وجود تبعية s1[i] ->T s1[i + 1].

for ( int i = 1 ; i < n ; ++ i ) { a [ i ] = a [ i - 1 ] + b [ i ]; // s1 c [ i ] += a [ i ]; // s2 }

يجب تنفيذ s1 بالتسلسل، بينما لا تعتمد s2 على حلقة تكرارية. يمكن تنفيذ s2 بالتوازي باستخدام DOALL Parallelism بعد إجراء جميع العمليات الحسابية اللازمة لـ s1 بالتسلسل. مع ذلك، يكون تحسين السرعة محدودًا في هذه الحالة. يتمثل النهج الأفضل في التوازي بحيث يتم تنفيذ s2 المقابل لكل s1 عند انتهاء s1.

يؤدي تطبيق التوازي المتسلسل إلى مجموعة الحلقات التالية، حيث يمكن تنفيذ الحلقة الثانية لفهرس بمجرد أن تنتهي الحلقة الأولى من فهرسها المقابل.

for ( int i = 1 ; i < n ; ++ i ) { a [ i ] = a [ i - 1 ] + b [ i ]; // s1 post ( i ); }for ( int i = 1 ; i < n ; i ++ ) { wait ( i ); c [ i ] += a [ i ]; // s2 }

لنفترض أن وقت تنفيذ s1 و s2 هوتيs1{\displaystyle T_{s_{1}}}وتيs2{\displaystyle T_{s_{2}}}إذن، يكون وقت تنفيذ الشكل التسلسلي للكود أعلاه هون(تيs1+تيs2){\displaystyle n\cdot (T_{s_{1}}+T_{s_{2}})}والآن، بفضل وجود التوازي في DOPIPE، يمكن تحقيق تسريع من خلال تنفيذ التكرارات بطريقة متسلسلة، مما يمنحنا وقت تنفيذ قدرهنتيs1+(نص)تيs2{\displaystyle n\cdot T_{s_{1}}+({\frac {n}{p}})\cdot T_{s_{2}}}، حيث يمثل p عدد المعالجات المتوازية.

انظر أيضاً

مراجع

  1. 1 2 3 4 5 سوليهين، يان (2016). أساسيات العمارة الموازية . بوكا راتون، فلوريدا: مطبعة اتفاقية حقوق الطفل. رقم ISBN 978-1-4822-1118-4.
  2. جوف، جينا (1991). "الاختبار العملي للاعتمادية". وقائع مؤتمر ACM SIGPLAN 1991 حول تصميم لغات البرمجة وتنفيذها - PLDI '91 . الصفحات 15-29 . doi : 10.1145/113445.113448 . ISBN  0897914287. S2CID 2357293 . 
  3. مورفي، نيال. "اكتشاف واستغلال التوازي في حلقات DOACROSS" (ملف PDF) . جامعة كامبريدج . تم الاطلاع عليه بتاريخ 10 سبتمبر 2016 .
  4. كافي، كريشنا. "توازي حلقات DOALL و DOACROSS - دراسة استقصائية" .{{cite journal}}يتطلب الاستشهاد بالمجلة ( مساعدة )|journal=
  5. أونيكريشنان، بريا (2012)، "نهج عملي للتوازي في DOACROSS"، المعالجة المتوازية Euro-Par 2012 ، سلسلة محاضرات في علوم الحاسوب، المجلد 7484، الصفحات 219-231 ، doi : 10.1007/978-3-642-32820-6_23 ، ISBN   978-3-642-32819-0، S2CID 18571258 
  6. "DoPipe: منهج فعال لموازاة المحاكاة" (ملف PDF) . إنتل . تم الاطلاع عليه بتاريخ 13 سبتمبر 2016 .