Przykład MPI: zbiór Mandelbrota

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

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

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

Część 4 życie programu

Zdalne wywoływanie procedur RPC. Dariusz Wawrzyniak 1

Zdalne wywoływanie procedur RPC

Zdalne wywoływanie procedur RPC

Co to jest sterta? Sterta (ang. heap) to obszar pamięci udostępniany przez system operacyjny wszystkim działającym programom (procesom).

Algorytmy równoległe. Rafał Walkowiak Politechnika Poznańska Studia inżynierskie Informatyka 2010

Programowanie współbieżne Wykład 2. Iwona Kochańska

// Liczy srednie w wierszach i kolumnach tablicy "dwuwymiarowej" // Elementy tablicy są generowane losowo #include <stdio.h> #include <stdlib.

Zdalne wywoływanie procedur RPC 27. października Dariusz Wawrzyniak (IIPP) 1

Temat: Dynamiczne przydzielanie i zwalnianie pamięci. Struktura listy operacje wstawiania, wyszukiwania oraz usuwania danych.

Zdalne wywoływanie procedur RPC 27. października 2010

Lab 9 Podstawy Programowania

do instrukcja while (wyrażenie);

// Potrzebne do memset oraz memcpy, czyli kopiowania bloków

Algorytmy równoległe: ocena efektywności prostych algorytmów dla systemów wielokomputerowych

Programowanie w C++ Wykład 5. Katarzyna Grzelak. 16 kwietnia K.Grzelak (Wykład 1) Programowanie w C++ 1 / 27

Zaawansowane programowanie w języku C++ Zarządzanie pamięcią w C++

Wskaźniki. Informatyka

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

Numeryczna algebra liniowa

Skalowalność obliczeń równoległych. Krzysztof Banaś Obliczenia Wysokiej Wydajności 1

Wskaźniki. Programowanie Proceduralne 1

Wykład 1_2 Algorytmy sortowania tablic Sortowanie bąbelkowe

Wstęp do Informatyki zadania ze złożoności obliczeniowej z rozwiązaniami

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

Programowanie - wykład 4

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

4. Tablica dwuwymiarowa to jednowymiarowa tablica wskaźników do jednowymiarowych tablic danego typu.

Zadania domowe. Ćwiczenie 2. Rysowanie obiektów 2-D przy pomocy tworów pierwotnych biblioteki graficznej OpenGL

Programowanie równoległe i rozproszone. Praca zbiorowa pod redakcją Andrzeja Karbowskiego i Ewy Niewiadomskiej-Szynkiewicz

I - Microsoft Visual Studio C++

Pętle i tablice. Spotkanie 3. Pętle: for, while, do while. Tablice. Przykłady

Podstawy programowania komputerów

Biblioteka standardowa - operacje wejścia/wyjścia

Stałe, tablice dynamiczne i wielowymiarowe

Lab 8. Tablice liczbowe cd,. Operacje macierzowo-wektorowe, memcpy, memmove, memset. Wyrażenie warunkowe.

Zasady programowania Dokumentacja

Algorytmy równoległe: ocena efektywności prostych algorytmów dla systemów wielokomputerowych

Pzetestuj działanie pętli while i do...while na poniższym przykładzie:

typ y y p y z łoż o on o e n - tab a lice c e w iel e owym m ar a o r we, e stru r kt k ury

Wymiar musi być wyrażeniem stałym typu całkowitego, tzn. takim, które może obliczyć kompilator. Przykłady:

Laboratorium 5: Tablice. Wyszukiwanie binarne

1. Liczby i w zapisie zmiennoprzecinkowym przedstawia się następująco

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

Programowanie w C++ Wykład 5. Katarzyna Grzelak. 26 marca kwietnia K.Grzelak (Wykład 1) Programowanie w C++ 1 / 40

Informatyka I. Wykład 3. Sterowanie wykonaniem programu. Instrukcje warunkowe Instrukcje pętli. Dr inż. Andrzej Czerepicki

Poniższe funkcje opisane są w 2 i 3 części pomocy systemowej.

Wykład 5_2 Algorytm ograniczania liczby serii za pomocą kopcowego rozdzielania serii początkowych

DYNAMICZNE PRZYDZIELANIE PAMIECI

1. Nagłówek funkcji: int funkcja(void); wskazuje na to, że ta funkcja. 2. Schemat blokowy przedstawia algorytm obliczania

Przesyłania danych przez protokół TCP/IP

Wstęp do Programowania, laboratorium 02

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

Metody Metody, parametry, zwracanie wartości

Wywoływanie procedur zdalnych

Programowanie Współbieżne. Algorytmy

Dla każdej operacji łącznie tworzenia danych i zapisu ich do pliku przeprowadzić pomiar czasu wykonania polecenia. Wyniki przedstawić w tabelce.

Podstawy informatyki. Informatyka stosowana - studia niestacjonarne. Grzegorz Smyk. Wydział Inżynierii Metali i Informatyki Przemysłowej

Informacje wstępne #include <nazwa> - derektywa procesora umożliwiająca włączenie do programu pliku o podanej nazwie. Typy danych: char, signed char

Równoległość i współbieżność

Równoległość i współbieżność

Instrukcje sterujące. Programowanie Proceduralne 1

Struktura programu. Projekty złożone składają się zwykłe z różnych plików. Zawartość każdego pliku programista wyznacza zgodnie z jego przeznaczeniem.

Strona główna. Strona tytułowa. Programowanie. Spis treści. Sobera Jolanta Strona 1 z 26. Powrót. Full Screen. Zamknij.

Podstawy programowania. Wykład 6 Wskaźniki. Krzysztof Banaś Podstawy programowania 1

Operacje grupowego przesyłania komunikatów

Konstrukcje warunkowe Pętle

Instrukcja wyboru, pętle. 2 wykład. Podstawy programowania - Paskal

Projektowanie algorytmów równoległych. Zbigniew Koza Wrocław 2012

Podstawy programowania. Wykład Pętle. Tablice. Krzysztof Banaś Podstawy programowania 1

Zadania z podstaw programowania obiektowego

Definicja. Ciąg wejściowy: Funkcja uporządkowująca: Sortowanie polega na: a 1, a 2,, a n-1, a n. f(a 1 ) f(a 2 ) f(a n )

Iteracje. Algorytm z iteracją to taki, w którym trzeba wielokrotnie powtarzać instrukcję, aby warunek został spełniony.

Zajęcia 6 wskaźniki i tablice dynamiczne

Programowanie w C++ Wykład 2. Katarzyna Grzelak. 5 marca K.Grzelak (Wykład 1) Programowanie w C++ 1 / 41

Wstęp do programowania INP001213Wcl rok akademicki 2018/19 semestr zimowy. Wykład 4. Karol Tarnowski A-1 p.

METODY I JĘZYKI PROGRAMOWANIA PROGRAMOWANIE STRUKTURALNE. Wykład 02

Algorytmy równoległe: prezentacja i ocena efektywności prostych algorytmów dla systemów równoległych

ANALIZA I INDEKSOWANIE MULTIMEDIÓW (AIM)

Wywoływanie procedur zdalnych

do MATLABa programowanie WYKŁAD Piotr Ciskowski

Kontrola przebiegu programu

WHILE (wyrażenie) instrukcja;

Podstawy informatyki. Informatyka stosowana - studia niestacjonarne. Grzegorz Smyk. Wydział Inżynierii Metali i Informatyki Przemysłowej

WHILE (wyrażenie) instrukcja;

Prof. Danuta Makowiec Instytut Fizyki Teoretycznej i Astrofizyki pok. 353, tel danuta.makowiec at gmail.com

Programowanie komputerowe. Zajęcia 2

Argumenty wywołania programu, operacje na plikach

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

Algorytmika i programowanie. Wykład 2 inż. Barbara Fryc Wyższa Szkoła Informatyki i Zarządzania w Rzeszowie

Aplikacja Sieciowa wątki po stronie klienta

for (inicjacja_warunkow_poczatkowych; wyrazenie_warunkowe; wyrazenie_zwiekszajace) { blok instrukcji; }

1 Podstawy c++ w pigułce.

5 Przygotował: mgr inż. Maciej Lasota

Transkrypt:

Przykład MPI: zbiór Mandelbrota 1

Zbiór Mandelbrota Zbiór punktów na płaszczyźnie zespolonej, które są quasi-stabilne, kiedy są iterowane funkcją: z k 1 = z k 2 c gdzie z k+1 wartość zespolona w k+1 iteracji, z k wartość w poprzedniej iteracji (z 0 =0), c badany punkt na płaszczyźnie zespolone. Stosując zasady mnożenia liczb zespolonych dostajemy wzory na update liczby z: 2 2 z real =z real z imag c real z imag =2z real z imag c imag gdzie imag i real oznaczają część całkowitą i urojoną liczby zespolonej. Iteracje prowadzi się do momentu, gdy: 2 z real 2 z imag 4 co oznacza że z rozbiegnie się do, lub gdy osiągniemy maksymalną liczbę iteracji. Jasność piksela równa 255 - liczba iteracji 2

Zapis obrazu na dysk void WritePGM(char *fname,int Width,int Height) { FILE *file=fopen(fname,"wt"); if (fname==null) return; fprintf(file,"p5 %d %d 255\n",Width,Height); for(int i=0;i<height;i++) fwrite(image[i],sizeof(char),width,file); fclose(file); Format.pgm (portable graymap obraz szary) jeden najprostszych formatów graficznych. Nagłówek postaci P5 szerokość wysokość maks_natężenie ; po nim bajty z których każdy z nich specyfikuje szarość jednego piksela. Standard MPI-2 zawiera własny interfejs do równoległego wejścia-wyjścia, być może o tym później. 3

Funkcja main void CalcImage(int Width,int Height) { for(int i=0;i<height;i++) CalcImgRow(Image[i],i,Width,Height); const int imgwidth=4096; const int imgheight=4096; int main() { AllocMem(imgWidth,imgHeight); CalcImage(imgWidth,imgHeight); WritePGM("mandel.pgm",imgWidth,imgHeight); FreeMem(imgWidth,imgHeight); return 0; Po kolei obliczamy wartości natężenia dla wszystkich wierszy. 4

Alokacja i dealokacja pamięci // dynamicznie alokowana tablica wskaźników char **Image; void AllocMem(int Width,int Height) { Image=new char *[Height]; for(int i=0;i<height;i++) Image[i]=new char[width]; // Analogicznie funkcja FreeMem zwalnia pamięć Obraz jest tablicą wskaźników o rozmiarze równym wysokości obrazu. i-ty element tablicy jest wskaźnikiem wskazującym na dane i-tego wiersza. i-ty wiersz to tablica liczb typu char o rozmiarze równym szerokości obrazu. W efekcie Image[i][j] zawiera jasność piksela o współrzędnych (i,j). 5

Obliczenia wartości piksela int CalcPixel (double x,double y) { double zre,zim,zoldre=0.0,zoldim=0.0; int i; for(i=0;i<255;i++) { zre=zoldre*zoldre-zoldim*zoldim+x; zim=2*zoldre*zoldim+y; if (zim*zim+zre*zre>=4.0) return i; zoldre=zre; zoldim=zim; return i; Funkcja sprawdza czy liczba zespolona x+y*i należy do zbioru Mandelbrota. Maksymalna liczba iteracji 255; jeżeli jest osiągnięta to przyjmujemy, że liczba jest elementem zbioru. Uwaga 1 : mając dane x i y nie wiemy, ile będziemy potrzebować iteracji. Uwaga 2: widać, że obliczenia dla jednego punktu wykonywane są niezależnie od jego sąsiadów. 6

Obliczenie wartości linii pikseli void CalcImgRow(char *Row,int num,int Width,int Height) { double y=-2.0+4.0*num/height; for(int i=0;i<width;i++) { double x=-2.0+4.0*i/width; // Jasność piksela proporcjonalna do liczby iteracji pętli Row[i]=(char)(255-CalcPixel(x,y)); 0 Width -2 2-2 0 Przeskalowanie współrzędnych pł. rzeczywista [0,Width]> -> [-2,2] pł. urojona [0,Height]> -> [-2,2] 2 Height 7

Pierwsze podejście do algorytmu równoległego Zauważmy, że wartość każdego piksela jest obliczana niezależnie od pozostałych. Jeden proces nadrzędny zbiera dane od procesów obliczeniowych - podrzędnych. Podzielmy obraz wierszami na liczbę części równą liczbie procesów podrzędnych. Każdy proces podrzędny przesyła swoje dane procesowi nadrzędnemu, który zapisuje wyniki do pliku. Ale uwaga: Liczba iteracji funkcji CalcPixel zależy od natężenia piksela. P 0 P 1 W przypadku podziału pracy obok P 1 musi wykonać dużo więcej obliczeń niż P 0 i P 2. - jest znacznie więcej jasnych pikseli. P 2 Po zakończeniu pracy P 0 i P 2 czekają bezproduktywnie na P 1. Na pewno nie uda się osiągnąć trzykrotnego przyspieszenia. 8

Funkcja main identyczna dla obydwu rozwiązań int main(int argc,char *argv[]) { MPI_Init(&argc,&argv); MPI_Comm_rank(MPI_COMM_WORLD,&rank); MPI_Comm_size(MPI_COMM_WORLD,&size); if (size<2) { printf("at least 2 processes are needed\n"); MPI_Finalize(); exit(-1); if (rank==0) { AllocMem(imgWidth,imgHeight); Master(); WritePGM("mandel.pgm",imgWidth,imgHeight); FreeMem(imgWidth,imgHeight); else Slave(); MPI_Finalize(); Proces nadrzędny wykonuje funkcje Master po czym zapisuje obraz na dysk Procesy podrzędne wykonują funkcję Slave. 9

Proces podrzędny alokacja statyczna void Slave() { int Slaves=size-1; int Slave=rank-1; int NRows=imgHeight/Slaves; int StartRow=Slave*NRows; int Remainder=imgHeight%Slaves; if (Slave<Remainder) { NRows++; StartRow+=Slave; else StartRow+=Remainder; for(int i=startrow;i<startrow+nrows;i++) { CalcImgRow(Row,i,imgWidth,imgHeight); MPI_Send(Row,imgWidth,MPI_CHAR,0,i,MPI_COMM_WORLD); Równomierny podział wierszy na procesy podrzędne. Dodatkowe korekty na wypadek gdy liczba wierszy nie dzieli się przez liczbę procesów podrzędnych. Po obliczeniu danych proces podrzędny wysyła je do procesu nadrzędnego, komunikat ten otrzymuje etykietę równą numerowi wiersza tak aby proces nadrzędny wiedział, który wiersz otrzymuje. 10

Proces nadrzędny alokacja statyczna void Master(){ MPI_Status Status; int Slaves=size-1; for (int i=1;i<size;i++) { int Slave=i-1; int NRows=imgHeight/Slaves; int StartRow=Slave*NRows; int Remainder=imgHeight%Slaves; if (Slave<Remainder) { NRows++; StartRow+=Slave; else StartRow+=Remainder; for(int j=0;j<nrows;j++) { MPI_Recv(Row,imgWidth,MPI_CHAR,i,MPI_ANY_TAG,MPI_COMM_WORLD, &Status); int ReceivedRow=Status.MPI_TAG; memcpy(image[receivedrow],row,sizeof(char)*imgwidth); Proces nadrzędny odpiera wiersze obrazu od kolejnych wierszy nadrzędnych. Każdy wiersz jest przesyłany odrębnym komunikatem 11

Alokacja statyczna - uwagi Rozwiązanie można znacznie zoptymalizować. Przesyłanie bloku wierszy jednym komunikatem. Proces nadrzędny również biorący udział w obliczeniach (obecnie zajmuje się wyłącznie odbiorem komunikatów). Obecna forma programu ma na celu najbardziej bezpośrednie porównanie z alokacją dynamiczną. 12

Statyczne równoważenie obciążenia P 1 P 2 P 3 P 4 Czas pracy przy idealnie zrównoważonym obciążeniu Dodatkowy czas z powodu niezrównoważonego obciążenia Cztery procesory, podział zadania obliczeniowego jeszcze przed przystąpieniem do pracy. Podział na cztery różne części. Czas obliczeń większy niż wynika z czterokrotnego przyspieszenia (trzy procesory czekają na najbardziej obciążony) 13

Dynamiczne równoważenie obciążenia P 1 P 2 P 3 P 4 Czas pracy przy idealnie zrównoważonym obciążeniu Dodatkowy czas z powodu niezrównoważonego obciążenia Dynamiczny podział zadania obliczeniowego w trakcie jego wykonywania. Każdy procesor, gdy staje się wolny zgłasza zapotrzebowanie i otrzymuje niewielki fragment pracy. Dzieje się tak do momentu gdy zadanie zostanie wykonane. Podobnie jak w poprzednim wypadku nie znamy rozmiaru fragmentu, ale ponieważ fragmenty są mniejsze to i mniejsze jest niezrównoważenie. 14

Model master-slave P 0 Proces nadrzędny (master) Numer wiersza P 1 Wynik wiersza... P N-2 P N-1 Procesy podrzędne (slave) Aplikacja MPI składa się z N procesów, (co najmniej dwóch) Proces o numerze 0 (master) zajmuje się rozsyłaniem numerów wierszy do procesów podrzędnych a następnie odebraniem od nich danych wiersza. Wada: proces master nie zajmuje się obliczeniami -> Maksymalne przyspieszenie dla trzech procesów: 2, dla czterech: 3. Procesy o numerach 1,...,N-2,N-1 zajmują się obliczaniem pikseli wierszy, których numery przesłał im proces nadrzędny. 15

Proces podrzędny alokacja dynamiczna void Slave() { int RowNum; MPI_Status Status; while(1) { MPI_Recv(&RowNum,1,MPI_INT,0,0,MPI_COMM_WORLD,&Status); if (RowNum==-1) return; CalcImgRow(Row,RowNum,imgWidth,imgHeight); MPI_Send(Row,imgWidth,MPI_CHAR,0,RowNum,MPI_COMM_WORLD); Proces podrzędny odbiera od procesu nadrzędnego numer wiersza. Numer wiersza równy -1 oznacza koniec pracy. Po obliczeniu danych wiersza wysyła je do procesu nadrzędnego, komunikat ten otrzymuje etykietę równą numerowi wiersza tak aby proces nadrzędny wiedział, który wiersz otrzymuje. 16

Proces nadrzędny - problemy Na początku należy zatrudnić wszystkie procesy podrzędne każdemu z nich wysyłamy numer wiersza. Procesy o numerach 1,..,N-1 (numer 0 to master) otrzymują numery wierszy 0,..,N-2. Założenie: Liczba procesów nadrzędnych jest mniejsza niż liczba wierszy. Następnie odbieramy od procesu nadrzędnego (dowolnego) dane wiersza i Jeżeli są nowe wiersze to wysyłamy kolejny Jeżeli nie ma nowych wierszy to wysyłamy numer -1 oznaczający koniec pracy Należy zadbać aby wszystkie procesy podrzędne otrzymały sygnał -1. W tym celu w zmiennej ActiveSlaves zliczamy aktywne procesy podrzędne 17

Proces nadrzędny - alokacja dynamiczna (1) void Master() { int NextRow,ActiveSlaves; MPI_Status Status; // rozesłanie wszystkim slave'om początkowych numerów wierszy for(int i=0;i<size-1;i++) MPI_Send(&i,1,MPI_INT,i+1,0,MPI_COMM_WORLD); // Liczba zatrudnionych procesów podrzędnych ActiveSlaves=size-1; // Numer następnego wiersza do wysłania NextRow=rank; do { // Odbierz wiersz danych od slave'a //(z dowolnego źródła i o dowolnej etykiecie) MPI_Recv(Row,imgWidth,MPI_CHAR,MPI_ANY_SOURCE,MPI_ANY_TAG, MPI_COMM_WORLD,&Status); ActiveSlaves--; // Numer odebranego wiersza to etykieta komunikatu int ReceivedRow=Status.MPI_TAG; 18

Proces nadrzędny alokacja dynamiczna (2) // Który slave przyszłał wiersz? int Slave=Status.MPI_SOURCE; // skopiuj wiersz do macierzy Image memcpy(image[receivedrow],row,sizeof(char)*imgwidth); // Czy są jeszcze wiersze do przesłania? if (NextRow<imgHeight) { MPI_Send(&NextRow,1,MPI_INT,Slave,0,MPI_COMM_WORLD); ActiveSlaves++; NextRow++; else { // Nie, wyślij -1 tzn sygnał zakończenia procesu podrzędnego int minusone=-1; MPI_Send(&minusOne,1,MPI_INT,Slave,0,MPI_COMM_WORLD); // Powtarzaj jak długo są akwywne procesy podrzędne while (ActiveSlaves>0); 19

Szczegółowa analiza (1) r - jest liczbą wierszy, u - liczbą pikseli w wierszu, p liczbą procesów, t W - średnim czasem generowania jednego wiersza. Przyjmujemy model czasu przesłania komunikatu pokazany na drugim wykładzie. Każdy numer wiersza musi być wysłany do procesora nadrzędnego, co łącznie zajmuje: t c1 =r t S 4 t B gdzie t S jest czasem opóźnienia, a t B czasem transmisji jednego bajtu. Również obliczone dane każdego wiersza, muszą być przesłane z powrotem, co łącznie zajmuje: t c2 =r t S u t B Przyjmujemy że dynamiczne równoważenie obliczeń jest tak dobre, że dzieli czas obliczania danych wierszy jest dzielony przez p-1. (Jeden procesor nie uczestniczy w obliczeniach). Łączny czas obliczeń jest w przybliżeniu równy: t o = r t w p 1 20

Szczegółowa analiza (2) Łączny czas pracy jest równy: t p =t c1 t c2 t o Zauważmy, że t c1 i t c2 nie zależą od p, natomiast t o jest dzielony w przybliżeniu przez p. Mamy więc sytuację odpowiadającą prawu Amdahla (pierwszy wykład), w którym przyspieszenie jest ograniczone z góry przez pewną wartość. Gdyby nie czas komunikacji, przyspieszenie byłoby równe p-1. Dla alokacji statycznej na pewno nie będzie p-1 krotnego przyspieszenia obliczeń (wzór na t o ) przyspieszenie procesory 21

Eksperymenty - czas pracy Mandelbrot - 4096x4096 5.5 Czas pracy 5 4.5 4 3.5 3 2.5 2 Dynamiczna Statyczna 1.5 1 Mandelbrot - 256x256 0.5 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 Liczba procesorów Czas pracy 0.02000 0.01800 0.01600 0.01400 0.01200 0.01000 0.00800 0.00600 0.00400 Dynamiczna Statyczna 0.00200 1 2 3 4 5 6 7 8 9 10 11 12 13 14 Liczba procesorów Dwa wymiary obrazu 4096x4096 oraz 256x256; klaster Mordor; sieć Infiniband 10Gb/s Czas dla dwóch procesorów czas wersji szeregowej 22

Przyspieszenie Przyspieszenie 13 12 11 10 9 8 7 6 5 4 3 2 1 4096 - dyn. 4096 - stat 256 - dyn. 256 - stat. 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 wersja szeregowa T(2) T S Liczba procesorów Przypomnienie: przyspieszenie S(p) obliczamy dzieląc czas wersji szeregowej przez czas wersji równoległej wykorzystującej p procesorów. 23

Podsumowanie Wersja dynamiczna charakteryzuje się o wiele lepszą skalowalnością niż wersja statyczna. Dla wersji dynamicznej skalowalność jest gorsza w przypadku mniejszego zbioru danych. Czas obliczeń mniej dominuje nad czasem komunikacji. Dla wersji statycznej jest odwrotnie. Nie jest łatwo to wytłumaczyć (wpływ protokołu przesyłania długich komunikatów?) Podobnego raportu oczekuję w Państwa projekcie. Wersje statyczną można dalej optymalizować Proces master również prowadzi obliczenia. Przesyłanie danych kilku wierszy jednym komunikatem. Programy do ściągnięcia z mojej strony aragorn.pb.bialystok.pl/~wkwedlo 24