رسائل غير مؤجلة عديمة الجدوى في MPI: تحليلات خفيفة وتعليمية لأولئك الذين هم قليلاً "في الموضوع"

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



لدينا "المعطى:"


لذا ، فإن جوهر مهمتنا الحسابية بشكل أساسي هو مقارنة عدد المرات التي يكون فيها البرنامج الذي يستخدم عمليات النقل من نقطة إلى نقطة دون تأخير ، أسرع من البرنامج الذي يستخدم عمليات النقل من نقطة إلى نقطة. سنجري قياسات لمصفوفات الإدخال بأبعاد 64 ، 256 ، 1024 ، 4096 ، 8192 ، 16384 ، 65536 ، 262144 ، 1048576 ، 4194304 ، 16777216 ، 33554432 العناصر. بشكل افتراضي ، يقترح حلها بأربع عمليات. وهنا ، في الواقع ، ما سننظر فيه:



عند الإخراج ، يجب أن نحصل على ثلاثة ناقلات: Y1 و Y2 و Y3 ، والتي ستجمعها عملية الصفر. سأختبر هذا الأمر برمته على نظامي استنادًا إلى معالج Intel مع 16 غيغابايت من ذاكرة الوصول العشوائي. لتطوير البرامج ، سنستخدم تطبيق معيار MPI من الإصدار 9.0.1 من Microsoft (في وقت كتابة هذا التقرير) ، Visual Studio Community 2017 وليس فورتران.

العتاد


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

حظر الصرف


لحظر الرسائل من نقطة إلى نقطة ، سنستخدم الوظائف:

MPI_Send - تنفيذ حظر إرسال الرسائل ، أي بعد استدعاء الوظيفة ، يتم حظر العملية حتى يتم كتابة البيانات المرسلة إليها من ذاكرتها إلى ذاكرة التخزين المؤقت للنظام الداخلي لـ MPI ، وبعد ذلك تستمر العملية في العمل ؛
MPI_Recv - يقوم بتنفيذ حظر استلام الرسائل ، أي بعد استدعاء الوظيفة ، يتم حظر العملية حتى وصول البيانات من عملية الإرسال وحتى كتابة هذه البيانات بالكامل إلى المخزن المؤقت لعملية الاستلام بواسطة بيئة MPI.

الصرف المؤجل غير المحجوب


للرسائل المؤجلة غير المحجوبة من نقطة إلى نقطة ، سوف نستخدم الوظائف:

MPI_Send_init - في الخلفية تهيئ البيئة لإرسال البيانات التي ستحدث في بعض المستقبل وبدون أقفال.
MPI_Recv_init - تعمل هذه الوظيفة بشكل مشابه للوظيفة السابقة ، هذه المرة فقط لاستقبال البيانات ؛
MPI_Start - يبدأ عملية استقبال أو إرسال رسالة ، كما أنه يعمل في خلفية a.k.a. دون عرقلة
MPI_Wait - يُستخدم للتحقق ، وإذا لزم الأمر ، انتظر اكتمال إرسال رسالة أو استلامها ، ولكنه يمنع العملية فقط إذا لزم الأمر (إذا لم يتم إرسال البيانات أو "لم يتم استلامها"). على سبيل المثال ، تريد العملية استخدام البيانات التي لم تصل إليها بعد - ليس جيدًا ، لذلك نقوم بإدراج MPI_Wait أمام المكان الذي ستحتاج فيه إلى هذه البيانات (نقوم بإدراجه حتى إذا كان هناك ببساطة خطر تلف البيانات). مثال آخر ، بدأت العملية في نقل البيانات في الخلفية ، وبعد بدء نقل البيانات ، بدأت على الفور في تغيير هذه البيانات بطريقة ما - ليس جيدًا ، لذلك نقوم بإدراج MPI_Wait أمام المكان في البرنامج حيث يبدأ في تغيير هذه البيانات (هنا نقوم أيضًا بإدراجها حتى إذا هناك ببساطة خطر تلف البيانات).

وبالتالي ، فإن تسلسل المكالمات مع التبادل المؤجل غير المحجوب هو كما يلي:

  1. MPI_Send_init / MPI_Recv_init - تهيئة البيئة للاستقبال أو الإرسال
  2. MPI_Start - ابدأ عملية الاستلام / الإرسال
  3. MPI_Wait - نطالب بخطر التلف (بما في ذلك "أسفل" و "عدم الإبلاغ") للبيانات المرسلة أو المستلمة

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

الهندسة البينية


للتوضيح ، نقوم بإنشاء رسم بياني لإجراء العمليات الحسابية بأربع عمليات. في هذه الحالة ، يجب على المرء أن يحاول توزيع جميع العمليات الحسابية المتجهية بالتساوي نسبيًا على العمليات. إليك ما حصلت عليه:



انظر هذه المصفوفات T0-T2؟ هذه هي مخازن لتخزين النتائج الوسيطة للعمليات. أيضا ، على الرسم البياني عند إرسال الرسائل من عملية إلى أخرى ، في بداية السهم هو اسم الصفيف الذي يتم إرسال بياناته ، وفي نهاية السهم هو الصفيف الذي يتلقى هذه البيانات.

حسنًا ، متى أجابنا أخيرًا على الأسئلة:

  1. ما نوع المشكلة التي نحلها؟
  2. ما الأدوات التي سنستخدمها لحلها؟
  3. كيف سنحلها؟

يبقى فقط لحلها ...

"الحل:"


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

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



من يهتم برؤية الكود:

برنامج لمنع نقل البيانات
#include "pch.h" #include <iostream> #include <iomanip> #include <fstream> #include <mpi.h> using namespace std; void add(double *A, double *B, double *C, int n); void sub(double *A, double *B, double *C, int n); void mul(double *A, double *B, double *C, int n); void div(double *A, double *B, double *C, int n); int main(int argc, char **argv) { if (argc < 2) { return 1; } int n = atoi(argv[1]); int rank; double start_time, end_time; MPI_Status status; double *A = new double[n]; double *B = new double[n]; double *C = new double[n]; double *D = new double[n]; double *E = new double[n]; double *G = new double[n]; double *T0 = new double[n]; double *T1 = new double[n]; double *T2 = new double[n]; for (int i = 0; i < n; i++) { A[i] = double (2 * i + 1); B[i] = double(2 * i); C[i] = double(0.003 * (i + 1)); D[i] = A[i] * 0.001; E[i] = B[i]; G[i] = C[i]; } cout.setf(ios::fixed); cout << fixed << setprecision(9); MPI_Init(&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD, &rank); if (rank == 0) { start_time = MPI_Wtime(); sub(A, B, T0, n); MPI_Send(T0, n, MPI_DOUBLE, 1, 0, MPI_COMM_WORLD); MPI_Send(T0, n, MPI_DOUBLE, 2, 0, MPI_COMM_WORLD); div(T0, G, T1, n); MPI_Recv(T2, n, MPI_DOUBLE, 1, 0, MPI_COMM_WORLD, &status); add(T1, T2, T0, n); mul(T0, T1, T2, n); MPI_Recv(T0, n, MPI_DOUBLE, 2, 0, MPI_COMM_WORLD, &status); MPI_Send(T2, n, MPI_DOUBLE, 1, 0, MPI_COMM_WORLD); add(T0, T2, T1, n); MPI_Recv(T0, n, MPI_DOUBLE, 1, 0, MPI_COMM_WORLD, &status); MPI_Recv(T2, n, MPI_DOUBLE, 2, 0, MPI_COMM_WORLD, &status); end_time = MPI_Wtime(); cout << "Clock resolution: " << MPI_Wtick() << " secs" << endl; cout << "Thread " << rank << " execution time: " << end_time - start_time << endl; } if (rank == 1) { add(C, C, T0, n); MPI_Recv(T1, n, MPI_DOUBLE, 0, 0, MPI_COMM_WORLD, &status); MPI_Send(T0, n, MPI_DOUBLE, 0, 0, MPI_COMM_WORLD); mul(T1, G, T2, n); add(T2, C, T0, n); MPI_Recv(T1, n, MPI_DOUBLE, 2, 0, MPI_COMM_WORLD, &status); MPI_Send(T0, n, MPI_DOUBLE, 2, 0, MPI_COMM_WORLD); sub(T1, T0, T2, n); MPI_Recv(T0, n, MPI_DOUBLE, 0, 0, MPI_COMM_WORLD, &status); add(T0, T2, T1, n); MPI_Send(T1, n, MPI_DOUBLE, 0, 0, MPI_COMM_WORLD); } if (rank == 2) { mul(C, C, T0, n); MPI_Recv(T1, n, MPI_DOUBLE, 0, 0, MPI_COMM_WORLD, &status); MPI_Recv(T2, n, MPI_DOUBLE, 3, 0, MPI_COMM_WORLD, &status); MPI_Send(T0, n, MPI_DOUBLE, 1, 0, MPI_COMM_WORLD); MPI_Send(T0, n, MPI_DOUBLE, 0, 0, MPI_COMM_WORLD); add(T1, T2, T0, n); mul(T0, G, T1, n); MPI_Recv(T2, n, MPI_DOUBLE, 1, 0, MPI_COMM_WORLD, &status); mul(T1, T2, T0, n); MPI_Recv(T1, n, MPI_DOUBLE, 3, 0, MPI_COMM_WORLD, &status); mul(T0, T1, T2, n); MPI_Send(T2, n, MPI_DOUBLE, 0, 0, MPI_COMM_WORLD); } if (rank == 3) { mul(E, D, T0, n); MPI_Send(T0, n, MPI_DOUBLE, 2, 0, MPI_COMM_WORLD); sub(T0, B, T1, n); mul(T1, T1, T2, n); sub(T1, G, T0, n); mul(T0, T2, T1, n); MPI_Send(T1, n, MPI_DOUBLE, 2, 0, MPI_COMM_WORLD); } MPI_Finalize(); delete[] A; delete[] B; delete[] C; delete[] D; delete[] E; delete[] G; delete[] T0; delete[] T1; delete[] T2; return 0; } void add(double *A, double *B, double *C, int n) { for (size_t i = 0; i < n; i++) { C[i] = A[i] + B[i]; } } void sub(double *A, double *B, double *C, int n) { for (size_t i = 0; i < n; i++) { C[i] = A[i] - B[i]; } } void mul(double *A, double *B, double *C, int n) { for (size_t i = 0; i < n; i++) { C[i] = A[i] * B[i]; } } void div(double *A, double *B, double *C, int n) { for (size_t i = 0; i < n; i++) { C[i] = A[i] / B[i]; } } 

برنامج مؤجل لنقل البيانات غير المحجوبة
 #include "pch.h" #include <iostream> #include <iomanip> #include <fstream> #include <mpi.h> using namespace std; void add(double *A, double *B, double *C, int n); void sub(double *A, double *B, double *C, int n); void mul(double *A, double *B, double *C, int n); void div(double *A, double *B, double *C, int n); int main(int argc, char **argv) { if (argc < 2) { return 1; } int n = atoi(argv[1]); int rank; double start_time, end_time; MPI_Request request[7]; MPI_Status statuses[4]; double *A = new double[n]; double *B = new double[n]; double *C = new double[n]; double *D = new double[n]; double *E = new double[n]; double *G = new double[n]; double *T0 = new double[n]; double *T1 = new double[n]; double *T2 = new double[n]; for (int i = 0; i < n; i++) { A[i] = double(2 * i + 1); B[i] = double(2 * i); C[i] = double(0.003 * (i + 1)); D[i] = A[i] * 0.001; E[i] = B[i]; G[i] = C[i]; } cout.setf(ios::fixed); cout << fixed << setprecision(9); MPI_Init(&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD, &rank); if (rank == 0) { start_time = MPI_Wtime(); MPI_Send_init(T0, n, MPI_DOUBLE, 1, 0, MPI_COMM_WORLD, &request[0]);// MPI_Send_init(T0, n, MPI_DOUBLE, 2, 0, MPI_COMM_WORLD, &request[1]);// MPI_Recv_init(T2, n, MPI_DOUBLE, 1, 0, MPI_COMM_WORLD, &request[2]);// MPI_Recv_init(T0, n, MPI_DOUBLE, 2, 0, MPI_COMM_WORLD, &request[3]);// MPI_Send_init(T2, n, MPI_DOUBLE, 1, 0, MPI_COMM_WORLD, &request[4]);// MPI_Recv_init(T0, n, MPI_DOUBLE, 1, 0, MPI_COMM_WORLD, &request[5]);// MPI_Recv_init(T2, n, MPI_DOUBLE, 2, 0, MPI_COMM_WORLD, &request[6]);// MPI_Start(&request[2]); sub(A, B, T0, n); MPI_Startall(2, &request[0]); div(T0, G, T1, n); MPI_Waitall(3, &request[0], statuses); add(T1, T2, T0, n); mul(T0, T1, T2, n); MPI_Startall(2, &request[3]); MPI_Wait(&request[3], &statuses[0]); add(T0, T2, T1, n); MPI_Startall(2, &request[5]); MPI_Wait(&request[4], &statuses[0]); MPI_Waitall(2, &request[5], statuses); end_time = MPI_Wtime(); cout << "Clock resolution: " << MPI_Wtick() << " secs" << endl; cout << "Thread " << rank << " execution time: " << end_time - start_time << endl; } if (rank == 1) { MPI_Recv_init(T1, n, MPI_DOUBLE, 0, 0, MPI_COMM_WORLD, &request[0]);// MPI_Send_init(T0, n, MPI_DOUBLE, 0, 0, MPI_COMM_WORLD, &request[1]);// MPI_Recv_init(T1, n, MPI_DOUBLE, 2, 0, MPI_COMM_WORLD, &request[2]);// MPI_Send_init(T0, n, MPI_DOUBLE, 2, 0, MPI_COMM_WORLD, &request[3]);// MPI_Recv_init(T0, n, MPI_DOUBLE, 0, 0, MPI_COMM_WORLD, &request[4]);// MPI_Send_init(T1, n, MPI_DOUBLE, 0, 0, MPI_COMM_WORLD, &request[5]);// MPI_Start(&request[0]); add(C, C, T0, n); MPI_Start(&request[1]); MPI_Wait(&request[0], &statuses[0]); mul(T1, G, T2, n); MPI_Start(&request[2]); MPI_Wait(&request[1], &statuses[0]); add(T2, C, T0, n); MPI_Start(&request[3]); MPI_Wait(&request[2], &statuses[0]); sub(T1, T0, T2, n); MPI_Wait(&request[3], &statuses[0]); MPI_Start(&request[4]); MPI_Wait(&request[4], &statuses[0]); add(T0, T2, T1, n); MPI_Start(&request[5]); MPI_Wait(&request[5], &statuses[0]); } if (rank == 2) { MPI_Recv_init(T1, n, MPI_DOUBLE, 0, 0, MPI_COMM_WORLD, &request[0]);// MPI_Recv_init(T2, n, MPI_DOUBLE, 3, 0, MPI_COMM_WORLD, &request[1]);// MPI_Send_init(T0, n, MPI_DOUBLE, 1, 0, MPI_COMM_WORLD, &request[2]);// MPI_Send_init(T0, n, MPI_DOUBLE, 0, 0, MPI_COMM_WORLD, &request[3]);// MPI_Recv_init(T2, n, MPI_DOUBLE, 1, 0, MPI_COMM_WORLD, &request[4]);// MPI_Recv_init(T1, n, MPI_DOUBLE, 3, 0, MPI_COMM_WORLD, &request[5]);// MPI_Send_init(T2, n, MPI_DOUBLE, 0, 0, MPI_COMM_WORLD, &request[6]);// MPI_Startall(2, &request[0]); mul(C, C, T0, n); MPI_Startall(2, &request[2]); MPI_Waitall(4, &request[0], statuses); add(T1, T2, T0, n); MPI_Start(&request[4]); mul(T0, G, T1, n); MPI_Wait(&request[4], &statuses[0]); mul(T1, T2, T0, n); MPI_Start(&request[5]); MPI_Wait(&request[5], &statuses[0]); mul(T0, T1, T2, n); MPI_Start(&request[6]); MPI_Wait(&request[6], &statuses[0]); } if (rank == 3) { MPI_Send_init(T0, n, MPI_DOUBLE, 2, 0, MPI_COMM_WORLD, &request[0]); MPI_Send_init(T1, n, MPI_DOUBLE, 2, 0, MPI_COMM_WORLD, &request[1]); mul(E, D, T0, n); MPI_Start(&request[0]); sub(T0, B, T1, n); mul(T1, T1, T2, n); MPI_Wait(&request[0], &statuses[0]); sub(T1, G, T0, n); mul(T0, T2, T1, n); MPI_Start(&request[1]); MPI_Wait(&request[1], &statuses[0]); } MPI_Finalize(); delete[] A; delete[] B; delete[] C; delete[] D; delete[] E; delete[] G; delete[] T0; delete[] T1; delete[] T2; return 0; } void add(double *A, double *B, double *C, int n) { for (size_t i = 0; i < n; i++) { C[i] = A[i] + B[i]; } } void sub(double *A, double *B, double *C, int n) { for (size_t i = 0; i < n; i++) { C[i] = A[i] - B[i]; } } void mul(double *A, double *B, double *C, int n) { for (size_t i = 0; i < n; i++) { C[i] = A[i] * B[i]; } } void div(double *A, double *B, double *C, int n) { for (size_t i = 0; i < n; i++) { C[i] = A[i] / B[i]; } } 



الاختبار والتحليل


لنقم بتشغيل برامجنا للصفائف بأحجام مختلفة ونرى ما سيحدث. يتم تلخيص نتائج الاختبار في الجدول ، في العمود الأخير الذي نحسب ونكتب معامل التسارع ، والذي نحدده على النحو التالي: K accele = T ex. غير كتلة. كتلة T / .



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



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



هنا ، في الواقع ، البرنامج نفسه:

قياس الوقت
 #include "pch.h" #include <iostream> #include <iomanip> #include <Windows.h> #include <fstream> using namespace std; void add(double *A, double *B, double *C, int n); void sub(double *A, double *B, double *C, int n); void mul(double *A, double *B, double *C, int n); void div(double *A, double *B, double *C, int n); int main() { struct res { double add; double sub; double mul; double div; }; int i, j, k, n, loop; LARGE_INTEGER start_time, end_time, freq; ofstream fout("test_measuring.txt"); int N[12] = { 64, 256, 1024, 4096, 8192, 16384, 65536, 262144, 1048576, 4194304, 16777216, 33554432 }; SetConsoleOutputCP(1251); cout << "   loop: "; cin >> loop; fout << setiosflags(ios::fixed) << setiosflags(ios::right) << setprecision(9); fout << " : " << loop << endl; fout << setw(10) << "\n " << setw(30) << ".   (c)" << setw(30) << ".   (c)" << setw(30) << ".  (c)" << setw(30) << ".   (c)" << endl; QueryPerformanceFrequency(&freq); cout << "\n : " << freq.QuadPart << " " << endl; for (k = 0; k < sizeof(N) / sizeof(int); k++) { res output = {}; n = N[k]; double *A = new double[n]; double *B = new double[n]; double *C = new double[n]; for (i = 0; i < n; i++) { A[i] = 2.0 * i; B[i] = 2.0 * i + 1; C[i] = 0; } for (j = 0; j < loop; j++) { QueryPerformanceCounter(&start_time); add(A, B, C, n); QueryPerformanceCounter(&end_time); output.add += double(end_time.QuadPart - start_time.QuadPart) / double(freq.QuadPart); QueryPerformanceCounter(&start_time); sub(A, B, C, n); QueryPerformanceCounter(&end_time); output.sub += double(end_time.QuadPart - start_time.QuadPart) / double(freq.QuadPart); QueryPerformanceCounter(&start_time); mul(A, B, C, n); QueryPerformanceCounter(&end_time); output.mul += double(end_time.QuadPart - start_time.QuadPart) / double(freq.QuadPart); QueryPerformanceCounter(&start_time); div(A, B, C, n); QueryPerformanceCounter(&end_time); output.div += double(end_time.QuadPart - start_time.QuadPart) / double(freq.QuadPart); } fout << setw(10) << n << setw(30) << output.add / loop << setw(30) << output.sub / loop << setw(30) << output.mul / loop << setw(30) << output.div / loop << endl; delete[] A; delete[] B; delete[] C; } fout.close(); cout << endl; system("pause"); return 0; } void add(double *A, double *B, double *C, int n) { for (size_t i = 0; i < n; i++) { C[i] = A[i] + B[i]; } } void sub(double *A, double *B, double *C, int n) { for (size_t i = 0; i < n; i++) { C[i] = A[i] - B[i]; } } void mul(double *A, double *B, double *C, int n) { for (size_t i = 0; i < n; i++) { C[i] = A[i] * B[i]; } } void div(double *A, double *B, double *C, int n) { for (size_t i = 0; i < n; i++) { C[i] = A[i] / B[i]; } } 



عند بدء التشغيل ، يطلب منك إدخال عدد دورات القياس ، لقد اختبرت لمدة 10000 دورة. عند الإخراج ، نحصل على متوسط ​​النتيجة لكل عملية:



لقياس الوقت ، استخدمت QueryPerformanceCounter عالي المستوى. أوصي بشدة بقراءة هذه الأسئلة الشائعة بحيث تختفي معظم الأسئلة حول قياس الوقت باستخدام هذه الوظيفة من تلقاء نفسها. وفقا لملاحظاتي ، فإنه يتمسك بـ TSC (ولكن من الناحية النظرية قد لا يكون له) ، لكنه يعيد ، حسب المساعدة ، العدد الحالي من علامات العداد. لكن الحقيقة هي أن عدادي لا يستطيع جسديًا قياس الفترة الزمنية لـ 32 ns (انظر الصف الأول من جدول النتائج). هذه النتيجة ترجع إلى حقيقة أنه بين مكالمتين من علامات التجزئة QueryPerformanceCounter 0 أو مرور علامات 1. بالنسبة للصف الأول في الجدول ، يمكننا فقط استنتاج أن ما يقرب من ثلث نتائج 10000 تساوي علامة واحدة. لذا فإن البيانات الموجودة في هذا الجدول لـ 64 و 256 وحتى لعناصر 1024 هي شيء تقريبي تمامًا. الآن ، دعونا نفتح أيًا من البرامج ونحسب عدد العمليات الإجمالية لكل نوع يواجهه ، تقليديًا سنقوم "بنشر" كل شيء وفقًا للجدول التالي:



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



استنادًا إلى نتائج البيانات ، ننشئ رسمًا بيانيًا لثلاث وظائف: الأولى تصف التغيير في الوقت المنقضي في حظر التحويلات بين العمليات ، من عدد عناصر المصفوفات ، والثانية تصف التغيير في الوقت المنقضي في عمليات النقل المؤجلة غير المحظورة بين العمليات ، على عدد العناصر في المصفوفات ، والثالث يصف التغيير في الوقت ، تنفق على العمليات الحسابية ، من عدد عناصر المصفوفات:



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

الملخص


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

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


All Articles