Unnützes verzögertes, nicht blockierendes Messaging in MPI: Lichtanalysen und Tutorial für diejenigen, die ein wenig "im Thema" sind

Zuletzt musste ich eine weitere triviale Lernaufgabe von meinem Lehrer lösen. Bei der Lösung gelang es mir jedoch, auf Dinge zu achten, an die ich überhaupt nicht gedacht hatte, vielleicht haben Sie nicht einmal darüber nachgedacht. Dieser Artikel ist eher für Studenten und für alle hilfreich, die ihre Reise in die Welt der Parallelprogrammierung mit MPI beginnen.



Unsere "Gegebenheiten"


Das Wesentliche von uns ist also, im Wesentlichen das Rechenproblem, zu vergleichen, wie oft ein Programm, das nicht blockierende verzögerte Punkt-zu-Punkt-Übertragungen verwendet, schneller ist als eines, das blockierende Punkt-zu-Punkt-Übertragungen verwendet. Wir werden Messungen für Eingangsfelder mit Abmessungen von 64, 256, 1024, 4096, 8192, 16384, 65536, 262144, 1048576, 4194304, 16777216, 33554432 Elementen durchführen. Standardmäßig wird vorgeschlagen, es mit vier Prozessen zu lösen. Und hier eigentlich, und das werden wir berücksichtigen:



Am Ausgang sollten wir drei Vektoren haben: Y1, Y2 und Y3, die der Nullprozess sammeln wird. Ich werde das Ganze auf meinem System testen, das auf einem Intel-Prozessor mit 16 GB RAM basiert . Um Programme zu entwickeln, werden wir die Implementierung des MPI- Standards aus der Microsoft-Version 9.0.1 (zum Zeitpunkt der Erstellung dieses Dokuments ist relevant), Visual Studio Community 2017 und nicht Fortran verwenden.

Materiel


Ich möchte nicht im Detail beschreiben, wie die Funktionen des MPI funktionieren, die verwendet werden. Sie können dem immer folgen und in die Dokumentation schauen. Daher werde ich nur einen kurzen Überblick darüber geben, was wir verwenden werden.

Austausch blockieren


Für das Blockieren von Punkt-zu-Punkt-Messaging verwenden wir die Funktionen:

MPI_Send - Blockiert das Senden von Nachrichten, d. H. Nach dem Aufruf der Funktion wird der Prozess blockiert, bis die gesendeten Daten nicht aus seinem Speicher in den internen Puffer der MPI-Umgebung geschrieben werden, nachdem der Prozess weiter arbeitet.
MPI_Recv - Blockiert den Nachrichtenempfang, d. H. Nach dem Aufruf einer Funktion wird der Prozess blockiert, bis Daten vom sendenden Prozess empfangen werden und diese Daten vollständig in den Puffer des empfangenden Prozesses von der MPI-Umgebung geschrieben wurden.

Aufgeschobener, nicht blockierender Austausch


Für verzögertes, nicht blockierendes Point-to-Point-Messaging verwenden wir die Funktionen:

MPI_Send_init - bereitet das Medium im Hintergrund auf das Senden von Daten vor, die in Zukunft auftreten werden, und keine Sperren;
MPI_Recv_init - diese Funktion arbeitet ähnlich wie die vorherige, nur um Daten zu empfangen;
MPI_Start - Startet das Empfangen oder Senden einer Nachricht und läuft auch im Hintergrund ak.a. ohne zu blockieren;
MPI_Wait- Wird verwendet, um die Fertigstellung des Pakets zu überprüfen und ggf. zu warten oder eine Nachricht zu erhalten. Hier wird der Prozess ggf. nur blockiert (wenn die Daten "untererzeichnet" oder "nicht empfangen" sind). Zum Beispiel möchte ein Prozess Daten verwenden, die noch nicht erreicht wurden - nicht gut, deshalb fügen wir MPI_Wait vor dem Ort ein, an dem diese Daten benötigt werden (wir fügen sie auch ein, wenn nur die Gefahr einer Datenbeschädigung besteht). Ein anderes Beispiel: Der Prozess hat die Hintergrunddatenübertragung gestartet, und nach dem Start der Datenübertragung begann er sofort, die Daten irgendwie zu ändern - nicht gut. Daher fügen wir MPI_Wait vor der Stelle in das Programm ein, an der diese Daten zu ändern beginnen (wir fügen sie auch hier ein, wenn es besteht lediglich das Risiko der Datenkorruption).

Also semantisch Die Reihenfolge der Aufrufe für den verzögerten, nicht blockierenden Austausch lautet wie folgt:

  1. MPI_Send_init / MPI_Recv_init - bereiten Sie die Umgebung für den Empfang oder die Übertragung vor
  2. MPI_Start - Startet den Sende- / Empfangsprozess
  3. MPI_Wait - wir rufen an, wenn die Gefahr besteht, dass die übertragenen oder empfangenen Daten beschädigt werden (einschließlich "Reparieren" und "Unterpatching")

Ich habe auch MPI_Startall , MPI_Waitall in meinen Testprogrammen verwendet . Ihre Bedeutung ist im Wesentlichen dieselbe wie bei MPI_Start und MPI_Wait. Sie arbeiten nur mit mehreren Paketen und / oder Übertragungen. Dies ist jedoch weit von der gesamten Liste der Start- und Wartefunktionen entfernt. Es gibt mehrere weitere Funktionen zur Überprüfung der Vollständigkeit der Operationen.

Kommunikationsarchitektur zwischen den Prozessen


Der Übersichtlichkeit halber erstellen wir eine Grafik zur Durchführung von Berechnungen anhand von vier Prozessen. Gleichzeitig ist es notwendig zu versuchen, alle vektorarithmetischen Operationen relativ gleichmäßig auf die Prozesse zu verteilen. Folgendes habe ich:



Sehen Sie sich diese T0-T2-Arrays an? Dies sind Puffer zum Speichern von Zwischenergebnissen von Operationen. Ebenfalls in der Grafik bei der Übertragung von Nachrichten von einem Prozess zum anderen am Anfang des Pfeils ist der Name des Arrays, dessen Daten übertragen werden, und am Ende des Pfeils das Array, das diese Daten empfängt.

Nun, als wir endlich die Fragen beantworteten:

  1. Was für ein Problem lösen wir?
  2. Welche Tools werden wir verwenden, um das Problem zu lösen?
  3. Wie werden wir es lösen?

Es bleibt nur noch zu lösen ...

Unsere "Lösung:"


Als nächstes werde ich die Codes der beiden Programme vorstellen, die oben erörtert wurden, aber zuerst werde ich etwas näher erläutern, was und wie.

Alle vektoriellen Rechenoperationen wurden in separaten Prozeduren (add, sub, mul, div) ausgeführt, um die Lesbarkeit des Codes zu verbessern. Alle Eingabearrays werden gemäß den von mir angegebenen Formeln fast zufällig initialisiert . Da der Zero-Prozess die Ergebnisse der Arbeit aus allen anderen Prozessen zusammenstellt, arbeitet er am längsten. Daher ist es logisch, seine Arbeitszeit gleich der Programmausführungszeit zu betrachten (wie wir uns erinnern, ist das Interesse an: Arithmetik + Messaging) sowohl im ersten als auch im zweiten Fall. Wir werden die Zeitintervalle mit der Funktion MPI_Wtime messen und gleichzeitig habe ich beschlossen, die Auflösung zu bestimmen, die ich dort hatteMPI_Wtick (irgendwo in der Dusche hoffe ich, dass sie an meinen invarianten TSC gebunden werden. In diesem Fall bin ich sogar bereit, ihnen den Fehler zu vergeben, der mit der Aufrufzeit der Funktion MPI_Wtime zusammenhängt). Lassen Sie uns also alles zusammenstellen, worüber ich oben geschrieben habe, und in Übereinstimmung mit der Grafik werden wir schließlich diese Programme entwickeln (und natürlich werden wir auch debuggen).



Wer interessiert sich für den Code:

Programm mit blockierenden Datenübertragungen
#include"pch.h"#include<iostream>#include<iomanip>#include<fstream>#include<mpi.h>usingnamespacestd;
voidadd(double *A, double *B, double *C, int n);
voidsub(double *A, double *B, double *C, int n);
voidmul(double *A, double *B, double *C, int n);
voiddiv(double *A, double *B, double *C, int n);
intmain(int argc, char **argv){
	if (argc < 2)
	{
		return1;
	}	
	int n = atoi(argv[1]);
	int rank;
	double start_time, end_time;
	MPI_Status status;
	double *A = newdouble[n];
	double *B = newdouble[n];
	double *C = newdouble[n];
	double *D = newdouble[n];
	double *E = newdouble[n];
	double *G = newdouble[n];
	double *T0 = newdouble[n];
	double *T1 = newdouble[n];
	double *T2 = newdouble[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;
	return0;
}
voidadd(double *A, double *B, double *C, int n){
	for (size_t i = 0; i < n; i++)
	{
		C[i] = A[i] + B[i];
	}
}
voidsub(double *A, double *B, double *C, int n){
	for (size_t i = 0; i < n; i++)
	{
		C[i] = A[i] - B[i];
	}
}
voidmul(double *A, double *B, double *C, int n){
	for (size_t i = 0; i < n; i++)
	{
		C[i] = A[i] * B[i];
	}
}
voiddiv(double *A, double *B, double *C, int n){
	for (size_t i = 0; i < n; i++)
	{
		C[i] = A[i] / B[i];
	}
}

Programm mit verzögerter, nicht blockierender Datenübertragung
#include"pch.h"#include<iostream>#include<iomanip>#include<fstream>#include<mpi.h>usingnamespacestd;
voidadd(double *A, double *B, double *C, int n);
voidsub(double *A, double *B, double *C, int n);
voidmul(double *A, double *B, double *C, int n);
voiddiv(double *A, double *B, double *C, int n);
intmain(int argc, char **argv){
	if (argc < 2)
	{
		return1;
	}	
	int n = atoi(argv[1]);
	int rank;
	double start_time, end_time;
	MPI_Request request[7];
	MPI_Status statuses[4];
	double *A = newdouble[n];
	double *B = newdouble[n];
	double *C = newdouble[n];
	double *D = newdouble[n];
	double *E = newdouble[n];
	double *G = newdouble[n];
	double *T0 = newdouble[n];
	double *T1 = newdouble[n];
	double *T2 = newdouble[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;
	return0;
}
voidadd(double *A, double *B, double *C, int n){
	for (size_t i = 0; i < n; i++)
	{
		C[i] = A[i] + B[i];
	}
}
voidsub(double *A, double *B, double *C, int n){
	for (size_t i = 0; i < n; i++)
	{
		C[i] = A[i] - B[i];
	}
}
voidmul(double *A, double *B, double *C, int n){
	for (size_t i = 0; i < n; i++)
	{
		C[i] = A[i] * B[i];
	}
}
voiddiv(double *A, double *B, double *C, int n){
	for (size_t i = 0; i < n; i++)
	{
		C[i] = A[i] / B[i];
	}
}



Prüfung und Analyse


Lassen Sie uns unsere Programme für Arrays unterschiedlicher Größe ausführen und sehen, was passiert. Die Testergebnisse sind in der letzten Spalte tabellarisch die Berechnung und Beschleunigungsfaktor schreiben , die wie folgt definiert ist: Mit dem Start = T exc. nicht blockieren. / T- Block



Wenn Sie sich diese Tabelle etwas genauer ansehen als üblich, werden Sie feststellen, dass der Beschleunigungsfaktor mit zunehmender Anzahl der verarbeiteten Elemente in etwa wie



folgt abnimmt: Versuchen wir herauszufinden, was los ist? Um dies zu tun, schlage ich vor, ein kleines Testprogramm zu schreiben, das die Zeit jeder Vektorarithmetikoperation misst und die Ergebnisse sorgfältig in eine gewöhnliche Textdatei reduziert.



Hier das Programm selbst:

Zeitmessung
#include"pch.h"#include<iostream>#include<iomanip>#include<Windows.h>#include<fstream>usingnamespacestd;
voidadd(double *A, double *B, double *C, int n);
voidsub(double *A, double *B, double *C, int n);
voidmul(double *A, double *B, double *C, int n);
voiddiv(double *A, double *B, double *C, int n);
intmain(){
	structres
	{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 = newdouble[n];
		double *B = newdouble[n];
		double *C = newdouble[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");
	return0;
}
voidadd(double *A, double *B, double *C, int n){
	for (size_t i = 0; i < n; i++)
	{
		C[i] = A[i] + B[i];
	}
}
voidsub(double *A, double *B, double *C, int n){
	for (size_t i = 0; i < n; i++)
	{
		C[i] = A[i] - B[i];
	}
}
voidmul(double *A, double *B, double *C, int n){
	for (size_t i = 0; i < n; i++)
	{
		C[i] = A[i] * B[i];
	}
}
voiddiv(double *A, double *B, double *C, int n){
	for (size_t i = 0; i < n; i++)
	{
		C[i] = A[i] / B[i];
	}
}



Beim Start fragt sie nach der Anzahl der einzugebenden Messzyklen, die ich auf 10.000 Zyklen getestet habe. Bei der Ausgabe erhalten wir das Durchschnittsergebnis für jede Operation:



Um die Zeit zu messen, habe ich einen QueryPerformanceCounter auf hoher Ebene verwendet . Ich empfehle dringend, diese FAQ zu lesenso dass die meisten Fragen zur Zeitmessung durch diese Funktion von selbst verschwinden. Nach meinen Beobachtungen hängt es an TSC (theoretisch kann es aber nicht dafür sein), gibt aber laut Zertifikat die aktuelle Anzahl der Ticks des Zählers zurück. Tatsache ist jedoch, dass mein Zähler das Zeitintervall von 32 ns physisch nicht messen kann (siehe erste Zeile der Ergebnistabelle). Dieses Ergebnis ist auf die Tatsache zurückzuführen, dass zwischen zwei Aufrufen des QueryPerformanceCounter 0 Ticks und dann 1 übergehen. Für die erste Zeile der Tabelle können wir nur feststellen, dass etwa ein Drittel der 10.000 Ergebnisse einem Tick entspricht. Die Daten in dieser Tabelle für 64, 256 und sogar für 1024 Elemente sind also ziemlich ungefähr.Lassen Sie uns nun eines der Programme öffnen und berechnen, wie viele Operationen insgesamt für jeden Typ, auf den er trifft, "die nächste Tabelle" "verbreiten":



Schließlich kennen wir die Zeit jeder vektoriellen Rechenoperation und wie viel in unserem Programm, versuchen Sie herauszufinden, wie viel Zeit dafür aufgewendet wird Diese Operationen sind in parallelen Programmen und wie viel Zeit wird für das Blockieren und den ausstehenden nicht blockierenden Datenaustausch zwischen Prozessen aufgewendet, und der Übersichtlichkeit halber legen wir ihn in die Tabelle:



Basierend auf den Ergebnissen der Daten zeichnen wir drei Funktionen auf: Die erste beschreibt die Änderung der Zeit für das Blockieren von Transfers zwischen Prozessen, die Anzahl der Arrayelemente, die zweite die Änderung der Zeit für verzögerte, nicht blockierende Transfers zwischen Prozessen für die Anzahl der Arrayelemente und die dritte die Änderung der Zeit Ausgaben für Rechenoperationen aus der Anzahl der Elemente von Arrays:



Wie Sie bereits festgestellt haben, ist die vertikale Skalierung des Diagramms logarithmisch, da dies eine notwendige Maßnahme ist Die Streuung der Zeiten ist zu groß und auf einem normalen Chart würden Sie überhaupt nichts sehen. Achten Sie auf die Funktion der Abhängigkeit der Rechenzeit von der Anzahl der Elemente. Die anderen beiden Funktionen werden erfolgreich um etwa 1 Million Elemente überholt. Die Sache ist, dass es im Unendlichen schneller wächst als seine beiden Gegner. Daher wird mit zunehmender Anzahl von verarbeiteten Elementen die Laufzeit von Programmen zunehmend durch die Arithmetik bestimmt und nicht durch Übertragungen. Angenommen, Sie haben die Anzahl der Übertragungen zwischen Prozessen erhöht, konzeptionell werden Sie feststellen, dass der Moment, in dem die Rechenfunktion die beiden anderen übernimmt, später eintritt.

Ergebnisse


Wenn Sie also die Länge der Arrays weiter erhöhen, werden Sie zu dem Schluss kommen, dass ein Programm mit verzögerten, nicht blockierenden Übertragungen nur um einiges schneller ist als das Programm, das den blockierenden Austausch verwendet. Wenn Sie die Länge der Arrays auf unendlich erhöhen (naja, oder nehmen Sie einfach zu lange Arrays), wird die Zeit Ihres Programms zu 100% durch Berechnungen abgelesen, und der Beschleunigungsfaktor strebt sicher nach 1.

Jetzt auch beliebt: