Wstęp do MPI-2. Jacek Dziedzic FTiMS, Politechnika Gdańska Gdańsk, Algorytmy rozproszone 2014/2015. v1.13



Podobne dokumenty
Wstęp do MPI-2. Jacek Dziedzic FTiMS, Politechnika Gdańska Gdańsk, Algorytmy rozproszone 2017/2018. v1.14

Programowanie w modelu przesyłania komunikatów specyfikacja MPI, cd. Krzysztof Banaś Obliczenia równoległe 1

Podstawy programowania. Wykład Funkcje. Krzysztof Banaś Podstawy programowania 1

Rozszerzenia MPI-2 1

Podstawy programowania skrót z wykładów:

W2 Wprowadzenie do klas C++ Klasa najważniejsze pojęcie C++. To jest mechanizm do tworzenia obiektów. Deklaracje klasy :

Szablony funkcji i szablony klas

KLASA UCZEN Uczen imię, nazwisko, średnia konstruktor konstruktor Ustaw Wyswietl Lepszy Promowany

utworz tworzącą w pamięci dynamicznej tablicę dwuwymiarową liczb rzeczywistych, a następnie zerującą jej wszystkie elementy,

Programowanie w modelu przesyłania komunikatów specyfikacja MPI. Krzysztof Banaś Obliczenia równoległe 1

PROE wykład 3 klasa string, przeciążanie funkcji, operatory. dr inż. Jacek Naruniec

Laboratorium Systemów Operacyjnych. Ćwiczenie 4. Operacje na plikach

5. Model komunikujących się procesów, komunikaty

Programowanie Obiektowo Zorientowane w języku c++ Przestrzenie nazw

Wskaźniki a tablice Wskaźniki i tablice są ze sobą w języku C++ ściśle związane. Aby się o tym przekonać wykonajmy cwiczenie.

Komunikacja za pomocą potoków. Tomasz Borzyszkowski

Lab 9 Podstawy Programowania

Wskaźniki i dynamiczna alokacja pamięci. Spotkanie 4. Wskaźniki. Dynamiczna alokacja pamięci. Przykłady

C++ - przeciążanie operatorów. C++ - przeciążanie operatorów. C++ - przeciążanie operatorów. C++ - przeciążanie operatorów

1 Pierwsze kroki w C++ cz.3 2 Obsługa plików

ang. file) Pojęcie pliku (ang( Typy plików Atrybuty pliku Fragmentacja wewnętrzna w systemie plików Struktura pliku

Wykład 4. Tablice. Pliki

Programowanie w C++ Wykład 8. Katarzyna Grzelak. 15 kwietnia K.Grzelak (Wykład 8) Programowanie w C++ 1 / 33

JĘZYKI PROGRAMOWANIA Z PROGRAMOWANIEM OBIEKTOWYM. Wykład 6

Argumenty wywołania programu, operacje na plikach

Biblioteka standardowa - operacje wejścia/wyjścia

Język C++ wykład VIII

System plików warstwa fizyczna

System plików warstwa fizyczna

System plików warstwa fizyczna

Programowanie w C++ Wykład 9. Katarzyna Grzelak. 14 maja K.Grzelak (Wykład 9) Programowanie w C++ 1 / 30

znajdowały się różne instrukcje) to tak naprawdę definicja funkcji main.

Wyjątki (exceptions)

KLASA UCZEN Uczen imię, nazwisko, średnia konstruktor konstruktor Ustaw Wyswietl Lepszy Promowany

Dariusz Brzeziński. Politechnika Poznańska, Instytut Informatyki

Programowanie Równoległe Wykład 5. MPI - Message Passing Interface. Maciej Matyka Instytut Fizyki Teoretycznej

IMIĘ i NAZWISKO: Pytania i (przykładowe) Odpowiedzi

Wykład PASCAL - Pliki tekstowe

Pliki. Funkcje tworzące pliki i operujące na nich opisane są w części 2 pomocy systemowej. Tworzenie i otwieranie plików:

Obsługa plików. Systemy Operacyjne 2 laboratorium. Mateusz Hołenko. 25 września 2011

Wykład II. Programowanie II - semestr II Kierunek Informatyka. dr inż. Janusz Słupik. Wydział Matematyki Stosowanej Politechniki Śląskiej

Obsługa wyjątków. Język C++ WW12

Techniki programowania INP001002Wl rok akademicki 2018/19 semestr letni. Wykład 3. Karol Tarnowski A-1 p.

1 Wskaźniki. 1.1 Główne zastosowania wskaźników

Pliki. Operacje na plikach w Pascalu

Wskaźnik może wskazywać na jakąś zmienną, strukturę, tablicę a nawet funkcję. Oto podstawowe operatory niezbędne do operowania wskaźnikami:

Projektowanie klas c.d. Projektowanie klas przykład

51. Metody komunikacji nieblokującej.

Konwersje napis <-> liczba Struktury, unie Scanf / printf Wskaźniki

Instrukcja do laboratorium Systemów Operacyjnych (semestr drugi)

Podczas dziedziczenia obiekt klasy pochodnej może być wskazywany przez wskaźnik typu klasy bazowej.

Operacje grupowego przesyłania komunikatów. Krzysztof Banaś Obliczenia równoległe 1

System plików warstwa logiczna

Programowanie Równoległe Wykład 4. MPI - Message Passing Interface. Maciej Matyka Instytut Fizyki Teoretycznej

Podstawy programowania. Wykład: 9. Łańcuchy znaków. dr Artur Bartoszewski -Podstawy programowania, sem 1 - WYKŁAD

Przekazywanie argumentów wskaźniki

1. Pierwszy program. Kompilator ignoruje komentarze; zadaniem komentarza jest bowiem wyjaśnienie programu człowiekowi.

Programowanie obiektowe i C++ dla matematyków

Programowanie w Sieci Internet Blok 2 - PHP. Kraków, 09 listopada 2012 mgr Piotr Rytko Wydział Matematyki i Informatyki

Funkcje zawarte w bibliotece < io.h >

4. Procesy pojęcia podstawowe

Programowanie w językach

Programowanie w języku C++

Logiczny model komputera i działanie procesora. Część 1.

Tablice (jedno i wielowymiarowe), łańcuchy znaków

Zajęcia nr 5 Algorytmy i wskaźniki. dr inż. Łukasz Graczykowski mgr inż. Leszek Kosarzewski Wydział Fizyki Politechniki Warszawskiej

Podstawy i języki programowania

Część 4 życie programu

Funkcje zawarte w bibliotece < io.h >

Wstęp do programowania INP001213Wcl rok akademicki 2017/18 semestr zimowy. Wykład 12. Karol Tarnowski A-1 p.

Pliki. Informacje ogólne. Obsługa plików w języku C

TEMAT : KLASY DZIEDZICZENIE

Tryby komunikacji między procesami w standardzie Message Passing Interface. Piotr Stasiak Krzysztof Materla

Ćwiczenie 4. Obsługa plików. Laboratorium Podstaw Informatyki. Kierunek Elektrotechnika. Laboratorium Podstaw Informatyki Strona 1.

Programowanie proceduralne INP001210WL rok akademicki 2018/19 semestr letni. Wykład 6. Karol Tarnowski A-1 p.

Podstawy informatyki. Elektrotechnika I rok. Język C++ Operacje na danych - wskaźniki Instrukcja do ćwiczenia

Wstęp do Programowania, laboratorium 02

System plików. Warstwowy model systemu plików

Informatyka I. Typy danych. Operacje arytmetyczne. Konwersje typów. Zmienne. Wczytywanie danych z klawiatury. dr hab. inż. Andrzej Czerepicki

Programowanie obiektowe. Materiały przygotował: mgr inż. Wojciech Frohmberg

Operacje na plikach. Informatyka. Standardowe strumienie wejścia i wyjścia

Typy wyliczeniowe Konwersje napis <-> liczba Struktury, unie Scanf / printf Wskaźniki

Informacje ogólne. Karol Trybulec p-programowanie.pl 1. 2 // cialo klasy. class osoba { string imie; string nazwisko; int wiek; int wzrost;

C++ - klasy. C++ - klasy. C++ - klasy. C++ - klasy. C++ - klasy WSKAŹNIKI KLASOWE

Klient-Serwer Komunikacja przy pomocy gniazd

public: // interfejs private: // implementacja // składowe klasy protected: // póki nie będziemy dziedziczyć, // to pole nas nie interesuje

Wskaźniki. nie są konieczne, ale dają językowi siłę i elastyczność są języki w których nie używa się wskaźników typ wskaźnikowy typ pochodny:

Zdalne wywoływanie procedur RPC. Dariusz Wawrzyniak 1

Wstęp do programowania obiektowego. WYKŁAD 3 Dziedziczenie Pola i funkcje statyczne Funkcje zaprzyjaźnione, this

Podstawy programowania w języku C++

System plików JFS. 1 Najważniejsze informacje. Mateusz Zakrzewski. 18 stycznia JFS to skrót od Journalled File System.

Obsługa plików. Laboratorium Podstaw Informatyki. Kierunek Elektrotechnika. Laboratorium Podstaw Informatyki Strona 1. Kraków 2013

Zaawansowane techniki programowania C#

Podstawy języka C++ Maciej Trzebiński. Praktyki studenckie na LHC IFJ PAN. Instytut Fizyki Jądrowej Polskiej Akademii Nauk. M. Trzebiński C++ 1/16

Zdalne wywoływanie procedur RPC

Niezwykłe tablice Poznane typy danych pozwalają przechowywać pojedyncze liczby. Dzięki tablicom zgromadzimy wiele wartości w jednym miejscu.

Zdalne wywoływanie procedur RPC

61 Topologie wirtualne

Tworzenie programów równoległych cd. Krzysztof Banaś Obliczenia równoległe 1

Szablony klas, zastosowanie szablonów w programach

Transkrypt:

Wstęp do MPI-2 Jacek Dziedzic FTiMS, Politechnika Gdańska Gdańsk, 2006-2015 Algorytmy rozproszone 2014/2015 v1.13

Pracujemy w systemie równoległym o architekturze MIMD, z pamięcią rozproszoną... N E T W O R K wiadomość #718 od: węzeł #1 do: węzeł #3 długość: 8192 B dane:...... serwer plików pamięć masowa Pamięć rozproszona każdy węzeł pracuje we własnej przestrzeni adresowej, nie może bezpośrednio "zajrzeć" do pamięci pozostałych węzłów. Komunikacja między węzłami odbywa się za pomocą przesyłania wiadomości po wydajnej sieci.

Pracujemy w systemie równoległym o architekturze MIMD, z pamięcią rozproszoną... N E T W O R K user@node2:~> ls plik1 plik2 plik3 user@node2:~> user@node3:~> ls plik1 plik2 plik3 user@node3:~> serwer plików plik1 pamięć masowa plik2 plik3 Dostęp do pamięci masowej zapewnia na ogół serwer plików, w sposób przezroczysty dla użytkownika.

MPI-2 W 1995 przeprowadzono ankietę wśród użytkowników MPI, pytając co chcieliby widzieć w kolejnej wersji standardu. Najczęściej padały prośby o dodanie: dynamicznego zarządzania procesami (startowanie nowych procesów w trakcie działania programu, etc.), wsparcia dla C++ (już z niego korzystamy), komunikacji jednostronnej (wszystkie informacje potrzebne do przesłania wiadomości dostarcza jedna strona, drugiej nie zawraca się głowy). Operacje typu Put i Get, pozwalające pisać do i czytać ze zdalnego węzła bez jego bezpośredniego udziału. równoległego we/wy. W 1997 pojawiła się specyfikacja MPI-2, w której zawarto wszystkie powyższe udogodnienia i jeszcze trochę (np. możliwe jest już wysyłanie wiadomości w Fortranie i jej odbiór w C++). Nie wszystkie implementacje MPI dysponują rozszerzeniami zawartymi w MPI-2, ale większość tak.

Po co nam równoległe we/wy? Najczęściej mamy do czynienia z sytuacją, w której każdy węzeł pracuje na porcji danych (dekompozycja danych). W pamięci masowej chcielibyśmy jednak mieć wszystkie dane w (uporządkowanej) całości. MAMY: CHCEMY: pamięć masowa

Bez równoległego we/wy metoda 1 Każdy węzeł może zapisywać dane do oddzielnego pliku. Łatwe do zrobienia, ale ma szereg wad: Podczas obróbki danych (post-processing) niewygodnie jest pracować z wieloma plikami zamiast z jednym. Co zrobić, jeśli liczba węzłów zmieni się pomiędzy kolejnymi wywołaniami programu? Przykładowo program pracował na 4 węzłach, wyprodukował 4 pliki wyjściowe, a jutro chcemy by pracował na 5 węzłach. Potrzeba dodatkowych użytków, które sklejałyby pliki do jednego po tym, jak program zakończy się strata czasu i przestrzeni dyskowej (co jeśli pliki mają po 4 GB?). Zapis do wielu plików jest niewydajny (seek, fragmentacja). storage

Bez równoległego we/wy metoda 2 Można zebrać dane na jednym węźle korzystając z przesyłania wiadomości i zapisać je z tego węzła. Gather(...) pamięć masowa

Bez równoległego we/wy metoda 2 Wady tego rozwiązania: Nierówny podział pracy węzeł, na którym piszemy do pliku jest zajęty wykonując we/wy, podczas gdy pozostałe węzły nudzą się. Na węźle odpowiedzialnym za we/wy potrzebujemy mnóstwo dodatkowej pamięci, żeby pomieścić na raz dane ze wszystkich węzłów (jeśli korzystamy z Gather). Można przesyłać po kawałku i pisać po kawałku, ale wtedy pozostałe węzły są dłużej zajęte (czekają z przesłaniem reszty aż master zapisze poprzednią porcję). Obciążenie sieci teraz wszystkie dane muszą przejść przez węzeł master, a być może bezpośrednie połączenia węzeł-serwer plików są krótsze.

MPI-2 oferuje równoległe we/wy wszystkie węzły piszą jednocześnie do jednego pliku MPI::File::Write(...) Wygodne (dostajemy jeden plik). Dobry podział pracy (wszystkie węzły zajęte). Efektywne (MPI i SO mogą optymalizować zapisy i odczyty). Podobnie dla odczytu. pamięć masowa

Równoległe we/wy jak? Skoncentrujmy uwagę na najbardziej pospolitej czynności wczytaniu danych z jednego pliku na wiele węzłów tak, że każdy z nich dostaje fragment danych. plik... node #0 node #1 node #2 Trzy metody: przesuń wskaźnik pliku i czytaj, czytaj-od-miejsca (read_at), widoki plików. node #(N-1)

Równoległe we/wy otwarcie pliku Zaczynamy od równoległego otwarcia pliku: static MPI::File MPI::File::Open(const MPI::Intracomm& comm, const char *filename, int amode, const MPI::Info& info); To jest operacja zbiorowa wszystkie węzły wewnątrz komunikatora muszą ją wywołać. (Jeśli chcemy równolegle otworzyć plik na jednym procesorze, możemy skorzystać z komunikatora MPI::COMM_SELF). comm komunikator, filename nazwa otwieranego pliku. Może się tak zdarzyć, że różne węzły widzą ten sam plik pod różnymi nazwami, np. jeden węzeł może widzieć plik pod nazwą /local/home/janek/plik1 a inny jako /mnt/lab/home/ janek/plik1. Jeśli tak jest, każdy węzeł przekazuje swoją nazwę. info dodatkowe informacje, które moglibyśmy chcieć przekazać implementacji MPI. Jeśli nie chcemy, przekazujemy MPI::INFO_NULL). amode tryb dostępu (o tym za chwilę). Zwraca: uchwyt do pliku. Domyślnie błędy nie kończą się katastrofą (ważne na laborce).

Równoległe we/wy otwarcie pliku Tryb dostępu jest bitową kombinacją poniższych flag: MPI_MODE_RDONLY tylko do odczytu, MPI_MODE_WRONLY tylko do zapisu, MPI_MODE_RDWR do odczytu i zapisu, MPI_MODE_CREATE utwórz plik, jeśli nie istnieje, MPI_MODE_EXCL sygnalizuj błąd, jeśli nie istnieje, MPI_MODE_DELETE_ON_CLOSE usuń plik po zamknięciu (dla plików tymczasowych), MPI_MODE_UNIQUE_OPEN obietnica, że plik nie będzie jednocześnie otwarty gdzieś indziej (pozwala SO na lepszą optymalizację), MPI_MODE_SEQUENTIAL tylko dostęp sekwencyjny (taśmy, etc.), MPI_MODE_APPEND przewiń na koniec pliku po otwarciu. Zadana kombinacja musi mieć sens, np. MPI_MODE_RDONLY MPI_MODE_WRONLY nie ma sensu. W Fortranie flagi te dodaje się (bo nie ma operatora bitowego OR). Wszystkie węzły muszą przekazać ten sam tryb dostępu. Parametr info dodatkowe wskazówki, które chcemy przekazać MPI, np. atrybuty pliku, jeśli jest tworzony, sugerowany rozmiar bufora, wskazówki co do tego, czy plik będzie głównie czytany czy pisany, etc.. Informacje te pozwalają implementacji na lepszą optymalizację późniejszych operacji na pliku. Na ogół nie będziemy przekazywali żadnych dodatkowych informacji przekazujemy wówczas MPI::INFO_NULL.

Równoległe we/wy zamykanie pliku Gdy skończymy pracę z plikiem, trzeba go zamknąć: void MPI::File::Close(); Zatem wołamy metodę Close() na rzecz obiektu klasy MPI::File, który reprezentuje nasz plik i który otrzymaliśmy jako wynik działania funkcji MPI::File::Open(). To też jest operacja zbiorowa wszystkie węzły komunikatora muszą jednocześnie zamykać plik. Musimy zamknąć wszystkie pliki zanim wywołamy MPI::Finalize(). W momencie zamknięcia pliku musimy zagwarantować, że wszystkie nieblokujące operacje we/wy zakończyły się (my mówimy tylko o blokujących operacjach we/wy).

Równoległe we/wy czytamy z pliku Odczyt wygląda podobnie do odbierania wiadomości: void MPI::File::Read(void* buf, int count, const MPI::Datatype& datatype, MPI::Status& status); NIE jest operacją grupową (!) nie wszystkie węzły komunikatora muszą czytać. Jeśli chcemy czytać z pliku tylko na niektórych węzłach, wywołujemy MPI::File::Read() tylko na tych węzłach. Istnieje wersja grupowa: MPI::File::Read_all(), która służy do odczytu na wszystkich węzłach komunikatora jednocześnie. Jeśli czytamy na wszystkich węzłach, korzystniej jest zastosować wersję grupową z uwagi na możliwość optymalizacji odczytu przez MPI i SO. buf adres bufora do którego czytamy dane. count liczba elementów (nie bajtów!) do wczytania. datatype typ odczytywanych danych (MPI::DOUBLE, MPI::INT, etc.) status struktura statusu, która zawiera szczegóły odczytu, tak samo jak w przypadku Recv(), np. liczbę faktycznie odczytanych elementów.

Równoległe we/wy uwagi dot. odczytu Wskaźniki pozycji pliku są niezależne na wszystkich węzłach. Oznacza to, że odczyt na jednym węźle nie ma wpływu na stan pliku (w szczególności na wskaźnik pozycji pliku) na pozostałych węzłach: plik A B Po tym jak węzeł A dokonał odczytu jego wskaźnik pozycji pliku przesuwa się, ale wskaźnik pozycji na B pozostaje niezmieniony. plik A B

W równoległym we/wy w MPI-2 dane w pliku są określonego typu nie liczymy danych w bajtach, tylko w elementach: Odczyt w stylu C (POSIX) read(... ile bajtów...) Zapis w stylu C (POSIX) write(... ile bajtów...) Zapis w stylu C++ std::read(... ile bajtów...) Zapis w stylu C++ std::write(... ile bajtów...) ale Równoległe we/wy uwagi dot. odczytu MPI-2 MPI::File::Read(... ile elementów, jakiego typu...) MPI-2 MPI::File::Write(... ile elementów, jakiego typu...) operujemy na elementach, nie bajtach. zwalnia nas to z konieczności pamiętania czy np. sizeof(int)==2, 4 czy 8 na danej platformie. jeśli w pliku są dane nie tylko jednego typu tworzymy typ danych użytkownika (np. rekord).

Równoległe we/wy uwagi dot. odczytu Istnieje przeciążona wersja operacji Read() pozbawiona ostatniego argumentu (status) korzystamy z niej jeśli status nas nie interesuje. Liczbę odczytanych elementów możemy wydobyć ze statusu za pomocą metody Get_count(). Kontrolą błędów musimy się zająć sami domyślnie operacje równoległego we/wy nie zgłaszają wyjątków ani nie kończą programu.

TU

Równoległe we/wy zapis do pliku Analogicznie do odczytu (i podobnie do wysłania wiadomości): void MPI::File::Write(const void* buf, int count, const MPI::Datatype& datatype, MPI::Status& status); Podobnie jak poprzednio: NIE jest operacją zbiorową (!) nie wszystkie węzły komunikatora muszą pisać. Jeśli jednak piszemy na wszystkich węzłach, warto skorzystać ze zbiorowej operacji MPI::File::Write_all() pozwoli ona MPI optymalizować zapis. Argumenty jak dla MPI::File::Read(), tylko bufor jest const. Jak przy odczycie każdy węzeł ma swój, niezależny wskaźnik zapisu. Sygnalizacja problemu: co jeśli dwóch jednocześnie pisze w to samo miejsce? Omówione operacje odczytu i zapisu nie mają zastosowania do plików otwartych w trybie MPI_MODE_SEQUENTIAL, te trzeba czytać i pisać z użyciem współdzielonych wskaźników pozycji pliku (shared file pointers) nie będziemy się nimi zajmować.

Równoległe we/wy jak przemieszczać się w pliku? Chcemy pisać/czytać w różnych miejscach pliku na różnych węzłach, musimy więc mieć możliwość przesuwania wskaźnika pozycji pliku (przemieszczania się w pliku, seek): void MPI::File::Seek(MPI::Offset offset, int whence); Składnia podobnia do POSIX lseek() używanej do przemieszczania w pliku w szeregowym we/wy. offset liczba bajtów (wyjątkowo nie elementów) o którą przesuwamy wskaźnik względem whence. whence (ang. skąd) stała symboliczna określająca względem jakiego miejsca mierzymy przesunięcie. Do dyspozycji mamy MPI_SEEK_SET od początku pliku, MPI_SEEK_END od końca pliku, MPI_SEEK_CUR od bieżącej pozycji. Przykładowo: // ustaw wskaźnik po pierwszych 10 bajtach (0..9) mój_plik.seek(10, MPI_SEEK_SET); // cofnij się o 6 bajtów mój_plik.seek(-6, MPI_SEEK_CUR); // przesuń się na koniec pliku mój_plik.seek(0, MPI_SEEK_END);

Równoległe we/wy przykład input.dat #include <mpi.h> #include <iostream> using namespace std; int main(int argc, char** argv) { MPI::Init(argc,argv); węzeł #0 węzeł #1 węzeł #2 k*porcja...... węzeł węzeł #k #(N-1) int moj_numer = MPI::COMM_WORLD.Get_rank(); const int porcja=200; MPI::File plik; double bufor[porcja]; plik = MPI::File::Open(MPI::COMM_WORLD,"input.dat", MPI_MODE_RDONLY,MPI::INFO_NULL); plik.seek(moj_numer*porcja*sizeof(double),mpi_seek_set); plik.read(bufor,porcja,mpi::double); plik.close(); } MPI::Finalize();

Równoległe we/wy odczyt bez jawnego przemieszczania Druga metoda odczytu (lub zapisu) czytanie (lub zapis) od razu od konkretnego miejsca, bez jawnego przesuwania wskaźnika pliku: void MPI::File::Read_at(MPI::Offset offset, void* buf, int count, const MPI::Datatype& datatype, MPI::Status& status); Dodatkowy parametr (offset) mówi z którego miejsca pliku czytać/pisać. Parametr offset jest typu MPI::Offset chociaż zachowuje się jak liczba całkowita (na ogół jest synonimem int), to nie ma gwarancji że pozostanie tak w przyszłości. W związku z powyższym przesunięcia w pliku należy deklarować jako MPI::Offset, nie int. Znowu istnieją wersje zbiorowe: MPI::File::Read_at_all(), MPI::File::Write_at_all().

Metoda trzecia: widoki (file views) Najbardziej skomplikowana metoda, ale dająca największe możliwości. Koncepcja widoków zwalnia programistę od pamiętania, że każdy węzeł czyta z innej części pliku. Nałożenie widoku na plik działa jak zasłonięcie niektórych fragmentów pliku na każdym z węzłów. plik widok na węźle #0 widok na węźle #2 Każdy węzeł widzi tylko część pliku. Widoki można zmieniać zdejmować i nakładać nowe. widok na węźle #1 widok na węźle #3

Metoda trzecia: widoki (file views) Co więcej, fragmenty pliku widziane na każdym z węzłów nie muszą być ciągłe! Otwiera to systemowi operacyjnemu i implementacji MPI pole do wielu optymalizacji, zwłaszcza jeśli dane się przeplatają. plik widok na węźle #0 widok na węźle #1 widok na węźle #2 widok na węźle #3 W szeregowym we/wy każdy węzeł musiałby wczytać cały plik, po czym wyrzucić ¾ danych (albo wykonać mnóstwo operacji przemieszczenia). Gdy korzystamy z widoków MPI-2, implementacja wie, że plik trzeba odczytać tylko raz, po czym automagicznie rozparcelowuje dane pomiędzy węzły.

Metoda trzecia: widoki (file views) Możliwe jest również określenie, w których miejscach pliku znajdują się "dziury", jeśli mamy do czynienia z sytuacją, w której chcemy ukryć pewne dane przed wszystkimi węzłami. Implementacja będzie się starała zoptymalizować we/wy jeśli dziur takich będzie dużo. niedostępne na żadnym z węzłów plik nagłówek widok na węźle #0 widok na węźle #1 widok na węźle #2 widok na węźle #3 Każdy z węzłów widzi tylko to, co jest dla niego istotne, nie wie co dzieje się w pozostałych częściach pliku, nie musi jawnie przeskakiwać dziur widzi należną mu część jak mniejszy, ciągły plik. Bardzo wygodne, wydajne. Jedyna trudna część definiowanie (nakładanie) widoku.

Równoległe we/wy nakładanie widoku Widok nakładamy korzystając z metody void MPI::File::Set_view(MPI::Offset disp, const MPI::Datatype& etype, const MPI::Datatype& filetype, const char *datarep, const MPI::Info& info) Plik musi być uprzednio otwarty. disp przesunięcie w pliku, od którego zaczyna się widok dla tego procesu. Przesunięcie to jest (wyjątkowo) mierzone w bajtach, nie w elementach. Pozwala to na przeskoczenie nagłówka pliku, który dzięki temu może mieć dowolną długość. etype podstawowa jednostka danych w pliku, filetype opisuje, które części pliku są widoczne, datarep łańcuch określający wewnętrzny format pliku jeden z {"native", "internal", "external32"}. info dodatkowe informacje, które chcemy przekazać implementacji MPI, podobnie jak przy MPI::File::Open(). Na ogół będziemy przekazywać MPI::INFO_NULL, nie przejmując się tym.

Równoległe we/wy etype i filetype etype typ danych reprezentujący najmniejszą jednostkę informacji w pliku. Np. dla pliku zawierającego liczby całkowite, etype==mpi::int. Jeśli w pliku mamy bardziej skomplikowane struktury, korzystamy z typów danych definiowanych przez użytkownika (rekordów). filetype opisuje w jaki sposób w pliku rozłożone są interesujące dane i niepotrzebne "dziury" najczęściej jest to typ definiowany przez użytkownika. Jeśli w pliku nie ma dziur, filetype==etype. nagłówek plik - etype, np. MPI::DOUBLE - filetype

Widoki uwagi Po nałożeniu widoku wskaźnik pozycji w pliku dla każdego procesu ustawia się na początek widoku. Po nałożeniu widoku wszystkie odczyty, zapisy i pozycjonowania działają względem nałożonego widoku, np. mój_plik.seek(0, MPI_SEEK_SET) przesuwa wskaźnik na początek widoku, nie pliku. MPI::File::Set_view() jest operacją zbiorową wszyscy w obrębie komunikatora nakładają widok jednocześnie. Argument datarep i rozmiar typu etype muszą być takie same na każdym węźle. Przed nałożeniem widoku należy mieć pewność, że wszystkie operacje na pliku zakończyły się (dotyczy operacji nieblokujących). Argument datarep opisuje jak wygląda reprezentacja danych w pliku. Najprostszy wariant to taki, w którym dane w pliku są dokładną kopią danych w pamięci. Tryb ten wybieramy podając "native". Ma to tę zaletę, że działa szybko i zapisuje/czyta dane bez żadnych strat, bo nie ma konwersji. Wada jest taka, że nie da się tego stosować w środowiskach heterogenicznych na innej architekturze reprezentacja danych może być zupełnie inna. O pozostałych możliwościach ("internal", "external32") powiemy sobie przy temacie współoperatywności (jak zapewnić przenośność pliku między różnymi architekturami).

Załóżmy, że mamy do odczytania plik o następującym formacie: nagłówek: 16 bajtów, zawartość którego (dla uproszczenia) chcemy zignorować, 2000 liczb double przeznaczonych dla procesu 0, 2000 liczb double przeznaczonych dla procesu 1, 2000 liczb double przeznaczonych dla procesu 2. 0 15 15+2000*d 15+4000*d przesunięcia (disp) (w bajtach). d=sizeof(double) // otwórz plik const int count = 2000; MPI::File plik; plik = MPI::File::Open(MPI::COMM_WORLD, "test.dat", MPI_MODE_RDONLY, MPI::INFO_NULL); // oblicz początek widoku zależnie od numeru procesora d=sizeof(double); MPI::Offset disp=15+mój_numer*count*d; // wszystko w bajtach // wybierz typ danych w pliku. Rezygnujemy z "dziur", wobec // czego filetype == etype MPI::Datatype etype = MPI::DOUBLE; MPI::Datatype filetype = MPI::DOUBLE; // ustaw widok plik.set_view(disp, etype, filetype, "native", MPI::INFO_NULL); // czytaj swoją porcję double bufor[count]; plik.read(bufor, count, etype); Widoki przykład widok w proc 0 widok w proc 1 widok w proc 2

MPI-2: mniej istotne operacje na plikach Sprawdzenie rozmiaru pliku: MPI::Offset MPI::File::Get_size(); Operacja punktowa. Rozmiar zwracany jest w bajtach (sic!). Pamiętamy, że rozmiar jest typu MPI::Offset. Zaalokowanie dodatkowej przestrzeni w pliku: void MPI::File::Preallocate(MPI::Offset size); size nowy rozmiar pliku. Służy do powiększania pliku podanie size mniejszego od bieżącego rozmiaru pliku nie skraca go. Wymuszenie powiększenia pliku (potencjalnie) zapobiega fragmentacji, która może nastąpić gdy plik powiększamy po kawałku. Operacja grupowa wszyscy muszą podać to samo size. Skutkuje powiększeniem pliku do zadanego rozmiaru. Dane, o które powiększył się plik są niezdefiniowane. Nie ma sensu przy MPI_MODE_SEQUENTIAL.

Równoległe we/wy kontrola błędów W MPI dla Fortranu i C funkcje we/wy zwracają wartość (w Fortranie dodatkowy parametr, w C wartość zwracana), którą można sprawdzić żeby upewnić się, czy operacja we/wy zakończyła się sukcesem: int error; error=mpi_file_open(...); if(error) // nie udalo sie error=mpi_file_seek(...); if(error) // nie udalo sie error=mpi_file_read(...); if(error) // nie udalo sie //... W C++ korzystamy z wyjątków, żeby móc rozdzielić gałęzie kodu obsługujące sytuację w której wszystko idzie pomyślnie od sytuacji wyjątkowej: try { MPI::File plik = MPI::File::Open(...); plik.seek(...); plik.read(...); //... } catch(mpi::exception &e) { // nie udalo sie }

Równoległe we/wy kontrola błędów Aby wykorzystać mechanizm wyjątków w MPI-2 musimy zrobić dwie rzeczy. 1) Upewnić się, że ich obsługa jest włączona często jest tak, że biblioteka MPI musi być skompilowana z odpowiednimi opcjami, żeby włączyć obsługę wyjątków. Na ogół domyślnie obsługa ta jest wyłączona (żeby niekorzystający z nich mogli uniknąć niepotrzebnych narzutów). Dla przykładu na dzień dzisiejszy na olimpie korzystamy z wersji bez wkompilowanej obsługi wyjątków: [jaca@olimp ~]$ ompi_info grep exceptions C++ exceptions: no 2) Wymusić, aby wystąpienie błędów we/wy kończyło się zgłoszeniem wyjątku. W MPI mamy do dyspozycji trzy sposoby reakcji na błędy: MPI::ERRORS_ARE_FATAL natychmiastowe zakończenie programu w razie błędu, domyślnie stosowane dla błędów w przesyłaniu wiadomości, MPI::ERRORS_RETURN w razie błędu funkcja zwraca kod błędu (przydatne w C, Fortranie) domyślnie stosowane dla błędów we/wy, MPI::ERRORS_THROW_EXCEPTIONS w razie błędu zgłaszany jest wyjątek ten sposób reakcji na błędy interesuje nas w C++. Musimy zatem wymusić trzeci typ reakcji dla operacji we/wy.

Równoległe we/wy kontrola błędów Do zmiany reakcji na błędy operacji we/wy korzystamy z metody void MPI::File::Set_errhandler(const MPI::Errhandler& errhandler) const, której jako argument przekazujemy jedną z wartości MPI::ERRORS_ARE_FATAL, MPI::ERRORS_RETURN, MPI::ERRORS_THROW_EXCEPTIONS (bądź funkcję obsługi błędów stworzoną przez użytkownika), a wywołujemy ją na rzecz pliku, dla którego chcemy zmienić sposób reakcji na błędy. Jeśli obsługa wyjątków nie została włączona przy kompilacji biblioteki MPI, zachowanie programu po napotkaniu błędu będzie zależało od konkretnej implementacji MPI (np. wersja na olimpie wypisuje komunikat o błędzie i próbuje kontynuować pracę). Przy korzystaniu z wyjątków musimy mieć pewność, że zarówno bibliotekę MPI, jak i program z niej korzystający skompilowano tym samym kompilatorem oraz że kod napisany w innych językach (C, Fortran) będzie poprawnie propagował wyjątki do momentu wyłapania ich w kodzie w C++.

MPI-2: współoperatywność Zagadnienie współoperatywności (interoperability) operacji we/wy jak będą wyglądały operacje we/wy w systemach heterogenicznych? Dwa aspekty: 1. Fizyczny układ danych w pliku różne architektury zapisują dane na różne sposoby. Np. liczba 0x304AB7F2 na procesorach Intel x86 będzie zapisana jako cztery bajty: 0xF2 0xB7 0x4A 0x30 (tzw. konwencja little-endian), a na procesorach Motorola 68000 jako cztery bajty: 0x30 0x4A 0xB7 0xF2 (tzw. konwencja big-endian). MPI, ponieważ gwarantuje współoperatywność, musi dać do dyspozycji możliwość poprawnego odczytania na Motoroli pliku zapisanego kiedyś na Intelu (gorzej, w środowisku heterogenicznym ten sam plik może być jednocześnie pisany w jednym fragmencie przez Motorolę, a w innym przez Intela x86). 2. Ewentualne konwersje na jednej architekturze MPI::DOUBLE może mieć osiem a na innej sześć bajtów. Co więcej, jednocześnie z tego samego pliku mogą korzystać oba procesory. Należy zapewnić ew. przycinanie wartości przy przesyłaniu w jedną stronę i ew. poszerzanie jej przy przesyłaniu w drugą. Z drugiej strony niektóre programy są pisane z myślą tylko o środowiskach homogenicznych i nie obchodzą je zagadnienia współoperatywności dobrze, gdyby dało się z tego zrezygnować (i można).

MPI-2: współoperatywność Drugi aspekt współoperatywności mamy zagwarantowany automatycznie MPI samo dokonuje potrzebnych konwersji zarówno przy operacjach na plikach jak i przy przesyłaniu wiadomości. Pierwszy aspekt współoperatywności w operacjach we/wy osiągamy za pomocą widoków. void MPI::File::Set_view(, const char *datarep, ); datarep łańcuch określający wewnętrzny format pliku jeden z {"native", "internal", "external32"}. Tryb "native" oznacza rezygnację ze współoperatywności. Wszystkie dane w pliku są dokładną kopią danych w pamięci. Pliki tak zapisane nie działają w systemach heterogenicznych. Nie dają się czytać (bez jakiegoś ręcznego przekonwertowania) na maszynach, gdzie obowiązują inne konwencje. Zaletą jest prostota i szybkość (brak jakichkolwiek narzutów związanych z konwersją). Tryb "internal" wymusza zapis w pewnym (nieokreślonym dokładnie) formacie wspólnym dla wszystkich maszyn wykonujących program. Włączone są konwersje, zatem osiągamy aspekt drugi współoperatywności. Nie ma jednak gwarancji, że plik da się czytać w innych środowiskach (nie osiągamy aspektu pierwszego). Tryb "external32" wymusza zapis w dobrze określonym formacie, o którym mamy gwarancję że będzie taki sam we wszystkich środowiskach. Włączone są konwersje. Osiągamy oba aspekty współoperatywności kosztem największych narzutów czasowych.

Równoległe we/wy synchronizacja i spójność Dwa podstawowe problemy, typowe dla środowisk w których mamy do czynienia ze współbieżnością. 1.Procesor A wykonuje zapis do pliku F na pozycji k, po czym procesor B odczytuje z pliku F na pozycji k. Czy procesor B odczyta już nowozapisane dane? Problem w tym, że zakończenie się operacji zapisu na procesorze A nie oznacza, że dane faktycznie są w pliku (na skutek buforowania). A rozpoczyna zapis A koniec zapisu na A A wysyła wiadomość do B, że dane są gotowe A B odbiera wiadomość i zaczyna odczyt B czas bufory systemowe... ale w tym momencie danych nie ma jeszcze fizycznie na dysku dysk

Równoległe we/wy synchronizacja i spójność 2. Procesor A wykonuje zapis do pliku F na pozycji k, w tym samym czasie procesor B wykonuje zapis do tego samego pliku na tej samej pozycji. Co znajdzie się w pliku? Sposób traktowania powyższych przypadków semantyka spójności. Np. w standardzie POSIX (http://en.wikipedia.org/wiki/posix) zakłada się tzw. semantykę silnej spójności oznacza to ad 1: gdy zakończy się zapis, mamy gwarancję że nowe dane widziane są we wszystkich procesach (co niekoniecznie oznacza, że są już fizycznie na dysku, tylko że system dba o spójność buforowanych danych), ad 2: w sytuacji dwóch jednoczesnych zapisów mamy gwarancję, że dane będą pochodziły od jednego lub drugiego procesu (nie wiadomo którego), ale wiadomo, że nie będą zbitką danych z obydwu.

Równoległe we/wy synchronizacja i spójność MPI zakłada słabszą spójność niż POSIX (nie daje takich samych gwarancji). Pozwala to na lepszą optymalizację. Możliwe jest w MPI osiągnięcie spójności prawie tak silnej jak w POSIX wymuszając dostęp do pliku w trybie atomowym. Atomowość operacji (atomicity) = niepodzielność = gwarancja, że w trakcie wykonywania operacji we/wy inna operacja we/wy (np. zainicjowana przez inny węzeł) nie przeszkodzi tej wykonywanej. Dostęp do plików w trybie atomowym będzie "bezpieczniejszy" (w sensie silniejszych gwarancji), ale mniej wydajny. Włączyć/wyłączyć atomowość dla danego pliku możemy korzystając z: void MPI::File::Set_atomicity(bool flag); flag żądana atomowość (true włączona, false wyłączona). Ustawienie atomowości to operacja zbiorowa wszystkie procesy w grupie operującej na tym pliku muszą zażądać tego samego. Odnosi się tylko do przyszłych operacji na pliku dla operacji, które już się rozpoczęły wciąż obowiązuje poprzednia atomowość.

Szczegóły synchronizacji we/wy i spójności Niech będzie dany plik F, otwierany przez dwie grupy procesów. Niech FH a = {fh a1, fh a2,, fh an,} będzie zbiorem uchwytów do tego pliku (w C++: obiektów reprezentujących ten plik) widzianych w pierwszej grupie otwierającej plik. Niech FH b = {fh b1, fh b2,, fh an,} będzie zbiorem uchwytów do tego pliku (w C++: obiektów reprezentujących ten plik) widzianych w drugiej grupie otwierającej plik. Niech A 1 i A 2 będą dwiema operacjami dostępu do danych w pliku, a D 1 i D 2 obszarami w pliku na których działają operacje A 1 i A 2. Będziemy mówić, że operacje A 1 i A 2 nakładają się, jeśli D 1 D 2. Będziemy mówić, że operacje A 1 i A 2 są niezgodne, jeśli nakładają się i przynajmniej jedna realizuje zapis. Niech S fh będzie sekwencją operacji na uchwycie fh pliku (w C++: obiekcie reprezentującym plik), otoczonych operacjami synchronizacji MPI::File::Sync() (o której powiemy więcej później). Będziemy mówić, że sekwencja operacji S fh jest sekwencją zapisu, jeśli co najmniej jedna z operacji w S fh jest zapisem lub w inny sposób zmienia stan pliku (np. MPI::File::Preallocate). Mówimy, że osiągamy spójność sekwencyjną, jeśli zbiór operacji zachowuje się tak, jak gdyby były one wykonane szeregowo, a każda z nich była atomowa.

Szczegóły synchronizacji we/wy i spójności Będziemy mówić, że dwie sekwencje operacji S 1 i S 2 są niewspółbieżne, jeśli mamy gwarancję, że jedna sekwencja operacji całkowicie poprzedza drugą (w czasie). Mamy do rozpatrzenia trzy przypadki (od najprostszych): 1. Jeden procesor operuje na jednym pliku za pomocą jednego uchwytu (obiektu plikowego) fh a1 FH a. W trybie atomowym wszystkie operacje będą sekwencyjnie spójne. W trybie nieatomowym operacje będą sekwencyjnie spójne, jeśli będą niewspółbieżne lub zgodne (lub jedno i drugie). Tłumaczenie: Gdy operujemy na jednym pliku na jednym procesorze, nie ma problemów jeśli: albo włączymy atomowość, albo nie będziemy jednocześnie pisać do jednego miejsca w pliku i wykonywać innej operacji na tym obszarze pliku. Zatem jeśli dwa wątki na tym samym procesorze chcą pisać w to samo miejsce pliku, albo jeden pisze, a drugi czyta (z tego samego miejsca), to musimy działać w trybie atomowym.

Szczegóły synchronizacji we/wy i spójności 2. Dwa procesory z tej samej grupy operują na jednym pliku (otwartym w tej samej zbiorowej operacji MPI::File::Open), każdy za pomocą swojego uchwytu (obiektu plikowego), tj. pierwszy wykonuje sekwencję operacji S 1 zawierającą operacje A 1 na fh a1 FH a, a drugi S 2 zawierającą operacje A 2 na fh a2 FH a. Jeżeli dla wszystkich operacji A 1 żadna z operacji A 2 nie jest z nią niezgodna, to spójność sekwencyjna jest zagwarantowana. W razie niezgodności nie ma takiej gwarancji (inaczej niż w POSIX)! Gwarancję taką można wymusić wybierając tryb atomowy. Tłumaczenie: Gdy dwa procesory otworzą ten sam plik w jednej operacji otwarcia, po czym jeden zapisze dane w pewne miejsce pliku, a drugi będzie chciał czytać z tego miejsca (bądź pisać w to miejsce) to żeby nie było kłopotów potrzebne jest włączenie trybu atomowego. Jeśli nie włączymy atomowości, dane zapisane na jednym procesorze mogą nie być od razu widoczne na innych procesorach synchronizację możemy wymusić wywołując MPI::File::Sync() (ponieważ kończy to sekwencję operacji). Podobnie dla zapisów w to samo miejsce pliku jeśli nie włączymy atomowości, gdy dwa procesy będą pisały w to samo miejsce pliku (nawet nie w tym samym czasie!) nie mamy gwarancji co do tego, co znajdzie się w pliku. Włączając tryb atomowy mamy gwarancję jak-w-posix do pliku trafią dane z jednego z procesorów, choć nie wiadomo którego.

Szczegóły synchronizacji we/wy i spójności 3. Dwa procesory z różnych grup operują na jednym pliku, każdy za pomocą swojego uchwytu (obiektu plikowego), tj. pierwszy wykonuje operację A 1 na fh a1 FH a, a drugi A 2 na fh b2 FH b. Aby zagwarantować spójność sekwencyjną, należy skorzystać z MPI::File::Sync() oraz zapewnić niewspółbieżność sekwencji, w których występują operacje zapisu. Tłumaczenie: Nawet jeśli operacje zapisu są niewspółbieżne, musimy korzystać z MPI::File::Sync() jeśli chcemy, by w pliku znalazły się dane "niewymieszane". Nie możemy sobie pozwolić na współbieżne sekwencje zapisu, nawet do różnych części pliku. Dlaczego muszą być niewspółbieżne, przecież piszemy do różnych części pliku?! A B C D E F G H 1 2 4 5 6 1 2 C 4 5 6 G H 1 2 C D 5 6 G H A B C 4 5 6 G H Ilustracja problemu: Mamy dwa rozłączne, ale współbieżne zapisy. Na skutek tego, że operacje na dysku wykonywane na blokach (klastrach), dane mogą się wymieszać jeśli zapis odbywa się do tych samych bloków. "12" trafia do bloku 1., "456" jest na granicy bloków 1. i 2. Oczekiwany wynik mamy tu. Na skutek wyścigów (zapis jednego bloku wchodzi między odczyt a zapis drugiego bloku) możemy jednak dostać zarówno to, jak i to.

Szczegóły synchronizacji we/wy i spójności Widać, że jest to sprawa dość skomplikowana, zwłaszcza jeśli otwieramy plik w więcej niż jednej grupie procesów. Najłatwiej będzie otwierać plik jednocześnie tylko w jednej grupie. Wybierając tryb atomowy ułatwiamy sobie życie, ale być może kosztem wydajności. W najtrudniejszym przypadku (3) musimy ręcznie wymuszać synchronizację pliku. Osiągamy ją wołając: void MPI::File::Sync(); Wywołanie to skutkuje faktycznym zapisaniem wszystkich danych z buforów do pliku oraz tym, że kolejne odczyty z tego pliku na pewno "widzą" nowozapisane dane. Operacja zbiorowa. Trzeba zagwarantować uprzednie zakończenie nieblokujących operacji we/wy na pliku. Otwarcie i zamknięcie pliku również powodują jego synchronizację.

Problem przesyłania obiektów przez MPI. Przy przesyłaniu wiadomości, zapisie do pliku specyfikujemy jakiego typu są dane. Mamy do dyspozycji typy wbudowane i typy definiowane przez użytkownika, ale co zrobić jeśli chemy przesłać np. std::string albo std::vector<double> albo obiekt klasy, którą sami stworzyliśmy? Problem w tym, że przesłanie samych danych zawartych bezpośrednio wewnątrz obiektu to nie to samo, co skopiowanie obiektu. Przykładowo: class macierz { public: // operacje na macierzy private: double* dane; }; Drobiazg na koniec Przesłanie zawartości obiektu klasy macierz nie załatwia sprawy kopiuje tylko wskaźnik dane, a nie to, na co on wskazuje. Po stronie odbiorcy nie będzie danych. Ten sam problem "płytkiej kopii" mamy lokalnie, jeśli nie zadbamy o konstruktor kopiujący: macierz oryginal; macierz kopia = oryginal; // (jeśli nie ma k.k.) kopiuje wskaznik 'dane', nie '*dane' // zmiany w kopii zmieniają tez oryginał, ponieważ pamięć wskazywana przez // 'dane' jest wspólna.

Drobiazg na koniec Generalnie kopiowanie "brutalne" obiektów klas, które nie składają się z samych danych (takie to tzw. POD plain old data*) albo które same przydzielają pamięć albo jest nielegalne, albo nie ma sensu, albo jedno i drugie. class kłopot { public: klopot() : cos(0); private: int cos; }; klopot oops; Próba odtworzenia obiektu oops przez "brutalne" skopiowanie sizeof(kłopot) bajtów spod adresu &oops w inne miejsce (na inny obiekt klasy kłopot) jest nielegalna (zachowanie niezdefiniowane) od momentu dodania konstruktora klasa kłopot przestala byc POD. W konsekwencji nie można obiektu tej klasy przeslać za pomocą wiadomości, która kopiowałaby bajty składające się na obiekt oops. * definicja niezwykle uproszczona. Dokładny opis tego, kiedy typ danych jest POD, a kiedy nie: http://www.parashift.com/c++-faq-lite/intrinsic-types.html#faq-26.7

Drobiazg na koniec Podobnie z std::string czy std::vector to nie są typy POD, a do tego same przydzielają sobie pamięć ze sterty: string tekst = "Co sie stało?"; cout << "Długość łańcucha: " << tekst.length() << "\nsizeof(tekst): " << sizeof(tekst) << endl; Dlugość łańcucha: 13 Sizeof(tekst): 4 Dlatego nie możemy łańcuchów i wektorów w prosty sposób przesyłać za pomocą wiadomości. Jak sobie radzić? Jedna metoda nie używać ich, zastępując tablicami. Ale tablice są złe. Druga metoda na czas przesyłania, zamienić na tablice. Np. std::string daje metodę data(), która zwraca wskaźnik do wewnętrznej reprezentacji łańcucha (const char*) możemy wyjąć tekst.length() bajtów stamtąd i przesłać. Gorzej po stronie odbiorcy, bo dostajemy tam tablicę znaków ale jeśli wcześniej prześlemy długość tekstu oddzielną wiadomością, to możemy z tablicy z powrotem zrobić łańcuch (niestety pisać do *data() nie można). Trzeba utworzyć nowy łańcuch po stronie odbiorcy, do którego przez kopiowanie wstawimy odebrane znaki. Trochę roboty.

Drobiazg na koniec Dla std::vector jest odrobinę łatwiej mamy gwarancję, że kolejne elementy wektora są umieszczone kolejno w pamięci oraz że &(wek[0]) jest adresem pierwszego elementu. Podobnie jak dla łańcucha możemy wtedy wyjąć wek.size()*sizeof(wek[0]) bajtów spod adresu &(wek[0]) i przesłać wiadomością. Po stronie odbiorcy będzie więcej roboty trzeba będzie zrekonstruować wektor na podstawie odebranych danych. Najlepiej zamknąć te nieciekawe operacje wewnątrz jakichś funkcji, żeby nie powtarzać brzydkiego kodu w programie. Przykładowo możnaby stworzyć funkcje void sendstring(const string& s, int target_processor); string recvstring(int source_processor); Jeśli chodzi o klasy stworzone przez nas, to postępujemy jak przy serializacji do pliku czy do strumienia. Dodajemy klasie dwie operacje: pierwszą, która zamienia klasę na ciąg bajtów zawierających wszystko, co potrzebne do jej późniejszego odtworzenia; druga operacja pozwala na odtworzenie klasy na podstawie ciągu bajtów. Ciągi bajtów przesyłamy za pomocą wiadomości.