Komplexere DatentypenIn den ersten zwei Abschnitten haben wir einfache Datentypen verschickt. Aber genauso ist es möglich Strings und Vektoren der Standard Template Library (STL), Klassen oder eigene Datentypen (MPI) zu verschicken. Beginnen wir mit den Vektoren der STL. Vektor und String-KlasseDie Vektorklasse ist im Prinzip ein einfaches Array mit "Zusatzfunktionalität". Das heißt die Klasse verhält sich genauso wie ein Array. Damit kann es für das Senden mit MPI eingesetzt werden. Allerdings ist in der "Zusatzfunktionalität" das dynamische Wachsen des Vektors enthalten, das heißt, er kann eine unterschiedliche Anzahl von Elementen enthalten (Trifft auch auf ein zur Laufzeit Speicher allokierendes Array mit new zu). Die Anzahl der Elemente eines Vektors kann mittels size() abgefragt werden. std::vector<double> vec; //vec is filled with doubles... send(&vec[0], vec.size(), MPI::DOUBLE, dest, tag);Das Senden ist demnach relativ einfach: Man gibt die Adresse des ersten Elementes und deren Anzahl an. Der Rest ist wie gehabt. Beim Empfangen allerdings ergeben sich Schwierigkeiten, denn der Empfänger kennt nicht die Anzahl der zu empfangenden Elemente, da diese erst zur Laufzeit feststehen. Deshalb muss vor dem eigentlichen Empfangen (Füllen des Empfangspuffers) über einen speziellen MPI-Befehl die Eigenschaften der Nachricht abgefragt werden. Der MPI-Befehl lautet dafür Probe() beziehungsweise nicht-blockierend Iprobe(). Als Parameter müssen der Sender und das tag angegeben werden. Zusätzlich kann ein Status-Objekt übergeben werden, was in unserem Fall erforderlich ist, da in diesem nach dem Aufruf die benötigten Informationen des Senders enthalten sind. Beide Methoden noch einmal im Überblick: void MPI::Comm::Probe(int source, int tag) const; void MPI::Comm::Probe(int source, int tag, MPI::Status& status) const; bool MPI::Comm::Iprobe(int source, int tag) const; bool MPI::Comm::IProbe(int source, int tag, MPI::Status& status) const;Bei nicht-blockierenden Iprobe() wird true oder false zurück gegeben, welches anzeigt, ob eine Nachricht empfangen wurde oder nicht. Während also immer wieder geprüft wird, ob ein anderer Prozess etwas geschickt hat, ist es möglich weitere Berechnungen/Aufgaben abzuarbeiten. Ein Beispiel soll dies noch etwas verdeutlichen: 1 #include "MyMPI.hpp" 2 #include <iostream> 3 #include <vector> 4 5 using namespace MF; 6 using std::cout; 7 using std::endl; 8 9 int main(int argc, char* argv[]) 10 { 11 const MyMPI* mpi = MyMPI::instance(); 12 const int master = 0; 13 14 if (mpi->rank() == master) //master 15 { 16 std::vector<int> vec; 17 const int tag = 0; 18 19 for (int dest=1; dest<mpi->size(); ++dest) 20 { 21 vec.push_back(dest); 22 mpi->world().Send(&vec[0], static_cast<int>(vec.size()), MPI::INT, dest, tag); 23 } 24 } 25 else 26 { 27 MPI::Status status; 28 unsigned int counter = 0; 29 30 for(;;) 31 { 32 if(mpi->world().Iprobe(MPI::ANY_SOURCE, MPI::ANY_TAG, status)) 33 { 34 std::vector<int> vec; 35 const int msglen = status.Get_count(MPI::INT); 36 vec.resize(msglen); 37 mpi->world().Recv(&vec[0], msglen, MPI::INT, status.Get_source(), status.Get_tag()); 38 39 cout << *MyMPI::instance() << " received: "; 40 std::vector<int>::const_iterator it = vec.begin(); 41 for(; it!=vec.end(); ++it) 42 cout << *it << " "; 43 break; 44 } 45 // some things to do... 46 ++counter; 47 } 48 cout << " Counter: " << counter << endl; 49 } 50 return 0; 51 } Die Ausgabe sollte bei fünf Prozessen in etwa folgendes ergeben: process 1 of 5 running on host m13f_mobile2 received: 1 Counter: 58171 process 2 of 5 running on host m13f_mobile2 received: 1 2 Counter: 79660 process 3 of 5 running on host m13f_mobile2 received: 1 2 3 Counter: 138210 process 4 of 5 running on host m13f_mobile2 received: 1 2 3 4 Counter: 114643 Dabei dient der Zähler als eine Abschätzung über die "genutzte" Rechenzeit, bevor das Empfangen gestartet wurde (Auf einem Parallelrechner im Release-Modus sind alle Counter null!). In Zeile 32 werden statt eines festgelegten Senders und tags die Konstanten ANY_SOURCE und ANY_TAG verwendet. Damit wird jede eingehende Nachricht angenommen. Die Zeile 35 ist die Entscheidende. Hier wird die Anzahl der übertragenen "Top-Level-Elemente" erfragt. Anschließend muss der Vektor auf die entsprechende Größe erweitert werden, da sonst beim "richtigen" Empfangen in andere Speicherbereiche geschrieben wird, die nicht zum eigentlichen Vektor gehören. In ähnlicher Weise lassen sich Objekte der std::string-Klasse verschicken, was am besten an einem Beispiel zu erkennen ist. 13 if (mpi->rank() == master) //master 14 { 15 std::string str("a message from the master process"); 16 const int tag = 0; 17 18 for (int dest=1; dest<mpi->size(); ++dest) 19 { 20 mpi->world().Send(&str[0], static_cast<int>(str.size()), MPI::CHAR, dest, tag); 21 } 22 } 23 else 24 { 25 MPI::Status status; 26 unsigned int counter = 0; 27 28 for(;;) 29 { 30 if(mpi->world().Iprobe(MPI::ANY_SOURCE, MPI::ANY_TAG, status)) 31 { 32 std::string str; 33 const int msglen = status.Get_count(MPI::CHAR); 34 str.resize(msglen); 35 mpi->world().Recv(&str[0], msglen, MPI::CHAR, status.Get_source(), status.Get_tag()); 36 37 cout << *MyMPI::instance() << " received: "; 38 cout << str << " "; 39 break; 40 } 41 // some things to do... 42 ++counter; 43 } 44 cout << " Counter: " << counter << endl; 45 } Im Prinzip wird statt des Vektors ein String benutzt und der Datentyp wurde auf MPI::CHAR geändert. Das heißt, das jeder(?) zusammenhängende Datenbereich übertragen werden kann? Versenden von ObjektenProbieren wir deshalb doch einmal die Übertragung eines Objektes einer einfachen Klasse. Dazu soll als Beispiel die Klasse Exchange dienen, die als Datenmember ein Integer und einen Zeiger auf einen Integer besitzt: Exchange
1 class Exchange 2 { 3 int stack_; 4 int* heap_; 5 6 public: 7 Exchange(int stack, int heap) 8 : stack_(stack) 9 , heap_(new int(heap)) 10 {} 11 int getStack() const {return stack_;} 12 int getHeap() const {return *heap_;} 13 ~Exchange() 14 { 15 delete heap_; 16 } 17 }; Diese Klasse verwenden wir im folgenden Beispiel: 1 #include "MyMPI.hpp" 2 #include "P2PComm.hpp" 3 #include <iostream> 4 5 using std::cout; 6 using std::endl; 7 using namespace MF; 8 9 int main(int argc, char* argv[]) 10 { 11 const MyMPI* mpi = MyMPI::instance(); 12 const int master = 0; 13 14 if (mpi->rank() == master) //master 15 { 16 Exchange exchange(13,13); 17 const int tag = 0; 18 Transceiver<> transceiver; 19 20 for (int dest=1; dest<mpi->size(); ++dest) 21 { 22 transceiver.send(&exchange, sizeof(Exchange), MPI::BYTE, dest, tag); 23 } 24 } 25 else 26 { 27 const int from = master; 28 const int tag = 0; 29 MPI::Status status; 30 Transceiver<> transceiver; 31 Exchange exchange(0,0); 32 33 transceiver.recv(&exchange, sizeof(Exchange), MPI::BYTE, from, tag, status); 34 cout << " Stack: " << exchange.getStack() << endl; 35 cout << " Heap: " << exchange.getHeap() << endl; 36 } 37 return 0; 38 } Der Master verschickt Byte-weise sein Exchange-Objekt an die anderen Prozesse. Diese empfangen das Objekt in ihre vorher angelegten Dummy-Objekte. Die Ausgabe bei zwei Prozessen ergibt folgendes: Stack: 13 Heap: 0 Das Objekt wird also nicht vollständig übertragen. Nur die Elemente auf dem Stack werden ordnungesgemäß versendet. Das heißt, die Variable stack_ mit der Zahl 13 und der Inhalt des Zeigers heap_ werden übertragen. Das letztere ist sehr gefährlich, da der Zeiger jedes Prozesses umgebogen wird. Dieser zeigt somit auf eine andere Speicherzelle und beim Verlassen wird diese dann gelöscht. Zusammenfassend können also keine Objekte mit Variablen die auf den Heap Daten haben ohne Weiteres versendet werden. Dazu bedarf es dann einer expliziten Serialisierung. Abgeleitete DatentypenMPI unterstützt mit vielen unterschiedlichen Funktionen das Zusammenfassen von einfachen Datentypen zu einem Neuen. Damit wird vor Allem ein Kopieren von Daten in einen extra Puffer vermieden, denn es können auch nicht zusammenhängende Speicherbereiche zu einem Datentyp zusammengefasst werden. Mit dem MPI-2 Standard existieren acht verschiedene Möglichkeiten einen neuen Datentyp zu definieren:
Der einfachste Datentyp lässt sich mit Create_contiguous() erstellen. Mit diesen werden Datentypen gleichen Typs die hintereinander im Speicher stehen zusammengefasst. Im folgenden Beispiel wird dies am Erstellen eines neuen Datentyps Alphabet bestehend aus 26 Chars und der Null-Terminierung gezeigt. 1 #include "MyMPI.hpp" 2 #include "P2PComm.hpp" 3 #include <iostream> 4 5 using std::cout; 6 using std::endl; 7 using namespace MF; 8 9 int main(int argc, char* argv[]) 10 { 11 const MyMPI* mpi = MyMPI::instance(); 12 const int master = 0; 13 const unsigned int count = 26 + 1; 14 15 MPI::Datatype Alphabet = MPI::CHAR.Create_contiguous(count); 16 Alphabet.Commit(); 17 18 if (mpi->rank() == master) //master 19 { 20 char alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 21 22 const int tag = 0; 23 Transceiver<> transceiver; 24 25 for (int dest=1; dest<mpi->size(); ++dest) 26 { 27 transceiver.send(&alphabet, 1, Alphabet, dest, tag); 28 } 29 } 30 else 31 { 32 const int from = master; 33 const int tag = 0; 34 MPI::Status status; 35 Transceiver<> transceiver; 36 char toRecv[count]; 37 38 transceiver.recv(&toRecv, 1, Alphabet, from, tag, status); 39 40 cout << *mpi->instance() << " received: " << toRecv << endl; 41 } 42 43 Alphabet.Free(); 44 return 0; 45 } 46 Der neue Datentyp Alphabet wird in Zeile 15 abgeleitet vom Datentyp MPI::CHAR erstellt und mittels Commit() MPI als neuen Datentyp bekannt gegeben. Das Char-Array kann jetzt ganz normal versendet und empfangen werden. Wenn der Datentyp nicht mehr benötigt wird, sollte dieser wie in Zeile 43 wieder "abgemeldet" werden. Angenommen man teilt alle Buchstaben in Vierer-Gruppen ein (Y und Z bilden eine Zweier-Gruppe) und möchte von diesen immer nur die ersten beiden Buchstaben verschicken. Ein Ansatz wäre die benötigten Buchstaben in einen Puffer zu schreiben und diesen zu verschicken, um die Buchstaben beim Empfänger wieder an genau die gleiche Stelle einzufügen. Diese Möglichkeit ist allerdings nicht sehr effektiv. Mit Create_vector() bzw. Create_hvector kann diese Aufgabe eleganter gelöst werden. Beide benötigen drei Parameter: Der erste gibt die Anzahl von Blöcken und der zweite die Anzahl der Datentypen pro Block an. Hier wären dies 7 Blöcke a zwei Buchstaben. Nun fehlt noch der Abstand (stride) zwischen den Blöcken. Dieser wird immer ausgehend vom Anfang des Speicherbereiches berechnet. Da hier die Einteilung in Vierer-Gruppen vorliegt, ist der Abstand vier Elemente (Create_vector()) bzw. vier Byte (Create_hvector). 1 #include "MyMPI.hpp" 2 #include "P2PComm.hpp" 3 #include <iostream> 4 5 using std::cout; 6 using std::endl; 7 using namespace MF; 8 9 int main(int argc, char* argv[]) 10 { 11 const MyMPI* mpi = MyMPI::instance(); 12 const int master = 0; 13 const unsigned int cntAlpha = 26 + 1; 14 15 const unsigned int cntBlock = 7; 16 const unsigned int lenBlock = 2; 17 const unsigned int stride = 4; 18 MPI::Datatype Alphabet = MPI::CHAR.Create_vector(cntBlock, lenBlock, stride); 19 Alphabet.Commit(); 20 21 if (mpi->rank() == master) //master 22 { 23 char alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 24 25 const int tag = 0; 26 Transceiver<> transceiver; 27 28 for (int dest=1; dest<mpi->size(); ++dest) 29 { 30 transceiver.send(&alphabet, 1, Alphabet, dest, tag); 31 } 32 } 33 else 34 { 35 const int from = master; 36 const int tag = 0; 37 MPI::Status status; 38 Transceiver<> transceiver; 39 char toRecv[cntAlpha]; 40 41 transceiver.recv(&toRecv, 1, Alphabet, from, tag, status); 42 43 toRecv[26] = '\0';//explicit termination 44 cout << *mpi->instance() << " received: " << toRecv << endl; 45 } 46 47 Alphabet.Free(); 48 return 0; 49 } 50 Im vorherigen Beispiel ist die Länge der einzelnen Blöcke immer konstant. Mit den Befehlen Create_index() bzw Create_hindex() kann diese variiert werden. Der erste Parameter gibt wieder die Anzahl der Blöcke an, der zweite ist ein Array mit der jeweiligen Länge pro Block und der dritte ist ebenfalls ein Array mit den Abständen vom Beginn in Anzahl von Elementen bzw. Bytes. Im folgenden Beispiel wird das Alphabet in Einer-, Zweier- bis zu einer Sechser-Gruppe mit jeweils einem Element dazwischen ausgegeben. 1 #include "MyMPI.hpp" 2 #include "P2PComm.hpp" 3 #include <iostream> 4 5 using std::cout; 6 using std::endl; 7 using namespace MF; 8 9 int main(int argc, char* argv[]) 10 { 11 const MyMPI* mpi = MyMPI::instance(); 12 const int master = 0; 13 const unsigned int cntAlpha = 26 + 1; 14 15 const unsigned int cntBlock = 6; 16 const int lenBlock[cntBlock] = {1, 2, 3, 4, 5, 6}; 17 const int displacement[cntBlock] = {0, 2, 5, 9, 14, 20}; 18 MPI::Datatype Alphabet = MPI::CHAR.Create_indexed(cntBlock, &lenBlock[0], &displacement[0]); 19 Alphabet.Commit(); 20 21 if (mpi->rank() == master) //master 22 { 23 char alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 24 25 const int tag = 0; 26 Transceiver<> transceiver; 27 28 for (int dest=1; dest<mpi->size(); ++dest) 29 { 30 transceiver.send(&alphabet, 1, Alphabet, dest, tag); 31 } 32 } 33 else 34 { 35 const int from = master; 36 const int tag = 0; 37 MPI::Status status; 38 Transceiver<> transceiver; 39 char toRecv[cntAlpha]; 40 41 transceiver.recv(&toRecv, 1, Alphabet, from, tag, status); 42 43 toRecv[26] = '\0';//explicit termination 44 cout << *mpi->instance() << " received: " << toRecv << endl; 45 } 46 47 Alphabet.Free(); 48 return 0; 49 } 50 Natürlich ist noch eine weitere Steigerung möglich. Denn die Datentypen jedes Blockes können ebenfalls variieren. 1 #include "MyMPI.hpp" 2 #include "P2PComm.hpp" 3 #include <iostream> 4 5 using std::cout; 6 using std::endl; 7 using namespace MF; 8 9 int main(int argc, char* argv[]) 10 { 11 const MyMPI* mpi = MyMPI::instance(); 12 const int master = 0; 13 const unsigned int cntAlpha = 26 + 1; 14 15 const unsigned int cntBlock = 3; 16 const int lenBlock[cntBlock] = {3, 3, 3}; 17 const int displ[cntBlock] = {0, 6, 12}; 18 const MPI::Datatype type[cntBlock] = {MPI::CHAR, MPI::CHAR, MPI::CHAR}; 19 20 MPI::Datatype Alphabet = MPI::Datatype::Create_struct(cntBlock, &lenBlock[0], &displ[0], &type[0]); 21 Alphabet.Commit(); 22 23 if (mpi->rank() == master) //master 24 { 25 char alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 26 27 const int tag = 0; 28 Transceiver<> transceiver; 29 30 for (int dest=1; dest<mpi->size(); ++dest) 31 { 32 transceiver.send(&alphabet, 1, Alphabet, dest, tag); 33 } 34 } 35 else 36 { 37 const int from = master; 38 const int tag = 0; 39 MPI::Status status; 40 Transceiver<> transceiver; 41 char toRecv[cntAlpha]; 42 43 transceiver.recv(&toRecv, 1, Alphabet, from, tag, status); 44 45 toRecv[26] = '\0';//explicit termination 46 cout << *mpi->instance() << " received: " << toRecv << endl; 47 } 48 49 Alphabet.Free(); 50 return 0; 51 } 52
In den letzten Beispielen ist nur der Datentyp char als Elementtyp verwendet worden. Dieser Typ benötigt genau ein Byte Speicherplatz und hat eine Speicherausrichtung (Alignment) von einem Byte. Ein int
belegt im Allgemeinen vier Byte und ist auf vier Byte im Speicher ausgerichtet. Das heißt, ein Integer muss immer an einer durch vier teilbaren Speicheradresse beginnen. Für das Erstellen des neuen Datentyps ist die Speicheradresse der einzelnen Grunddatentypen wichtig, beziehungsweise wie groß das Displacement jedes Typs ist. Die Speicheradresse jedes Typs kann mit der MPI-Funktion MPI::Get_Address() berechnet werden. Subtrahiert man jetzt die Adresse vom ersten Datentyp vom zweiten, so erhält man dessen Displacement. Analog wird die Adresse des ersten Datentyps vom dritten subtrahiert. Für das obige Beispiele wird der char-Datenmember ein Displacement von null, der Integer von vier und das Double von acht erhalten. Auf diese Art und Weise wird das Programm Plattform unabhängig gehalten. Das folgende Beispiel zeigt das Erstellen und Versenden von Datenmembern einer Klasse, welche dem Byte-weisen Verschicken vorzuziehen ist. 1 #include "MyMPI.hpp" 2 #include "P2PComm.hpp" 3 #include <iostream> 4 5 using std::cout; 6 using std::endl; 7 using namespace MF; 8 9 int main(int argc, char* argv[]) 10 { 11 const MyMPI* mpi = MyMPI::instance(); 12 const int master = 0; 13 const int cntDt = 3; 14 15 struct SpecialData 16 { 17 char letter_; 18 unsigned int id_; 19 double value_; 20 21 SpecialData(char letter='\0', unsigned int id=0, double value=0.0) 22 : letter_(letter) 23 , id_(id) 24 , value_(value) 25 {} 26 }; 27 28 MPI::Datatype types[] = {MPI::CHAR, MPI::UNSIGNED, MPI::DOUBLE}; 29 30 int lenBlock[] = {1,1,1}; 31 SpecialData specialData; 32 MPI::Aint tmpDisp[] = { MPI::Get_address(&specialData.letter_), 33 MPI::Get_address(&specialData.id_), 34 MPI::Get_address(&specialData.value_)}; 35 36 MPI::Aint displacement[cntDt] = {}; 37 for(unsigned int i=1; i<cntDt; ++i) 38 { 39 displacement[i] = tmpDisp[i] - tmpDisp[0]; 40 } 41 42 MPI::Datatype buff_datatype = MPI::Datatype::Create_struct(3,&lenBlock[0],&displacement[0],&types[0]); 43 buff_datatype.Commit(); 44 45 if (mpi->rank() == master) //master 46 { 47 specialData.letter_ = 'a'; 48 specialData.id_ = mpi->rank(); 49 specialData.value_ = 13.0; 50 51 cout << *mpi->instance() << " send: " << specialData.letter_ << " "; 52 cout << specialData.id_ << " " << specialData.value_ << endl; 53 54 const int tag = 0; 55 Transceiver<> transceiver; 56 57 for (int dest=1; dest<mpi->size(); ++dest) 58 { 59 transceiver.send(&specialData, 1, buff_datatype, dest, tag); 60 } 61 } 62 else 63 { 64 const int from = master; 65 const int tag = 0; 66 MPI::Status status; 67 Transceiver<> transceiver; 68 69 transceiver.recv(&specialData, 1, buff_datatype, from, tag, status); 70 71 cout << *mpi->instance() << " send: " << specialData.letter_ << " "; 72 cout << specialData.id_ << " " << specialData.value_ << endl; 73 } 74 75 buff_datatype.Free(); 76 return 0; 77 } 78 Die letzten drei Funktionen MPI::Datatype::Create_darray() MPI::Datatype::Create_subarray() und MPI::Datatype::Create_indexed_block() werden im Kapitel Parallel I/O behandelt, da diese Funktionen unter anderem dem Speichern eines globalen, auf die einzelnen Prozesse verteilten Arrays in eine Datei dienen. |