-1 Oczywiście, że to zły pomysł. Nie będziesz mógł używać blas, lapack ani żadnej innej istniejącej biblioteki macierzy z takim formatem przechowywania. Ponadto wprowadzasz nieefektywności według danych nielokalnych i pośrednich
Thomas Klimpel,
9
@Thomas Czy to naprawdę uzasadnia zdanie negatywne?
akid
33
Nie głosuj za głosem. To uzasadnione pytanie, nawet jeśli jest to błędny pomysł.
Wolfgang Bangerth,
3
std :: vector nie jest wektorem rozproszonym, więc nie będziesz w stanie wykonywać z nim wielu obliczeń równoległych (oprócz maszyn z pamięcią współużytkowaną), zamiast tego użyj Petsc lub Trilinos. Co więcej, zwykle mamy do czynienia z rzadkimi matrycami, a macie pełne gęste macierze. Do zabawy z rzadkimi macierzami można użyć std :: vector <std :: map>, ale znowu, to nie działałoby bardzo dobrze, patrz post @WolfgangBangerth poniżej.
gnzlbg
3
spróbuj użyć std :: vector <std :: vector <double>> z MPI, a będziesz chciał zestrzelić siebie
pyCthon
Odpowiedzi:
43
To zły pomysł, ponieważ wektor musi przydzielić tyle obiektów w przestrzeni, ile macierzy w macierzy. Alokacja jest droga, ale przede wszystkim jest to zły pomysł, ponieważ dane macierzy istnieją teraz w wielu tablicach rozrzuconych wokół pamięci, a nie w jednym miejscu, w którym pamięć podręczna procesora może z łatwością uzyskać do niej dostęp.
Jest to również marnotrawny format przechowywania: std :: vector przechowuje dwa wskaźniki, jeden na początku tablicy i jeden na końcu, ponieważ długość tablicy jest elastyczna. Z drugiej strony, aby była to właściwa matryca, długości wszystkich rzędów muszą być takie same, więc wystarczyłoby przechować liczbę kolumn tylko raz, zamiast pozwolić, aby każdy wiersz przechowywał swoją długość niezależnie.
Jest to w rzeczywistości gorsze niż mówisz, ponieważ std::vectorprzechowuje trzy wskaźniki: początek, koniec i koniec przydzielonego obszaru pamięci (na przykład pozwalając nam zadzwonić .capacity()). Ta pojemność może różnić się od wielkości, co znacznie pogorszy sytuację!
user14717
18
Oprócz powodów, o których wspomniał Wolfgang, jeśli używasz vector<vector<double> >, będziesz musiał wyrejestrować go dwukrotnie za każdym razem, gdy chcesz odzyskać element, co jest bardziej kosztowne obliczeniowo niż pojedyncza operacja dereferencji. Jednym typowym podejściem jest zamiast tego przydzielenie jednej tablicy (a vector<double>lub a double *). Widziałem także, jak ludzie dodają cukier składniowy do klas macierzy, owijając wokół tej pojedynczej tablicy kilka bardziej intuicyjnych operacji indeksowania, aby zmniejszyć ilość „mentalnego obciążenia” potrzebnego do wywołania właściwych wskaźników.
@Wolfgang: W zależności od wielkości gęstej matrycy dwa dodatkowe wskaźniki na wiersz mogą być nieistotne. Jeśli chodzi o rozproszone dane, można pomyśleć o użyciu niestandardowego alokatora, który zapewnia, że wektory są w ciągłej pamięci. Tak długo, jak pamięć nie zostanie poddana recyklingowi, nawet standardowy alokator będzie nam przylegał do pamięci z odstępem o wielkości dwóch wskaźników.
@Geoff: Jeśli korzystasz z dostępu losowego i używasz tylko jednej tablicy, nadal musisz obliczyć indeks. Nie może być szybciej.
W moim systemie jest teraz wyraźny zwycięzca (kompilator gcc 4.7 z -O3)
odbitki vectormatrix czasu:
index 997:3
index 998:3
index 999:30xc7fc680xc7fc80
calc took:185.507 k=100000000
real 0m0.257s
user 0m0.244s
sys 0m0.008s
Widzimy również, że dopóki standardowy alokator nie przetwarza zwolnionej pamięci, dane są ciągłe. (Oczywiście po niektórych zwolnieniach nie ma na to gwarancji.)
odbitki macierzy czasu:
index 997:1
index 998:1
index 999:10x7ff41f208f480x7ff41f208f50
calc took:187.349 k=100000000
real 0m0.257s
user 0m0.248s
sys 0m0.004s
Piszesz „W moim systemie jest teraz wyraźny zwycięzca” - czy miałeś na myśli brak wyraźnego zwycięzcy?
akid
9
-1 Zrozumienie wydajności kodu HPC może być niepraktyczne. W twoim przypadku rozmiar matrycy po prostu przekracza rozmiar pamięci podręcznej, więc po prostu mierzysz przepustowość pamięci twojego systemu. Jeśli zmienię N na 200 i zwiększę liczbę iteracji do 1000, otrzymam „obliczenia wzięte: 65” vs „obliczenia wzięte: 36”. Jeśli dodatkowo zastąpię a = a * a przez + = a1 * a2, aby uczynić go bardziej realistycznym, otrzymam „calc wziął: 176” vs „calc wziął: 84”. Wygląda więc na to, że możesz stracić czynnik dwa pod względem wydajności, używając wektora wektorów zamiast macierzy. Prawdziwe życie będzie bardziej skomplikowane, ale wciąż jest to zły pomysł.
Thomas Klimpel,
tak, ale spróbuj użyć wektorów std :: z MPI, C wygrywa ręce w dół
pyCthon
4
Nie polecam tego, ale nie z powodu problemów z wydajnością. Będzie on nieco mniej wydajny niż tradycyjna macierz, która zwykle jest alokowana jako duża część ciągłych danych, które są indeksowane za pomocą pojedynczego wskaźnika dereferencji i arytmetyki liczb całkowitych. Powodem spadku wydajności są głównie różnice w buforowaniu, ale gdy rozmiar macierzy wystarczająco się powiększy, efekt zostanie amortyzowany, a jeśli użyjesz specjalnego alokatora dla wewnętrznych wektorów, aby były one wyrównane do granic pamięci podręcznej, to dodatkowo złagodzi problem buforowania .
Moim zdaniem nie jest to wystarczający powód, aby tego nie robić. Powodem dla mnie jest to, że powoduje to wiele bólów głowy związanych z kodowaniem. Oto lista bólów głowy, które spowodują w dłuższej perspektywie
Korzystanie z bibliotek HPC
Jeśli chcesz korzystać z większości bibliotek HPC, musisz iterować wektor i umieścić wszystkie ich dane w ciągłym buforze, ponieważ większość bibliotek HPC oczekuje tego jawnego formatu. Przychodzą mi na myśl BLAS i LAPACK, ale także wszechobecna biblioteka HPC MPI byłaby znacznie trudniejsza w użyciu.
Większy potencjał błędu kodowania
std::vectornic nie wie o swoich wpisach. Jeśli wypełnisz std::vectorwięcej std::vectors, Twoim zadaniem jest upewnić się, że wszystkie mają ten sam rozmiar, ponieważ pamiętaj, że chcemy, aby macierz i macierze nie miały zmiennej liczby wierszy (lub kolumn). Dlatego będziesz musiał wywoływać wszystkie poprawne konstruktory dla każdego wpisu zewnętrznego wektora, a każdy, kto używa twojego kodu, musi oprzeć się pokusie użycia std::vector<T>::push_back()dowolnego z wewnętrznych wektorów, co spowodowałoby uszkodzenie całego następującego kodu. Oczywiście możesz tego zabronić, jeśli piszesz poprawnie swoją klasę, ale o wiele łatwiej jest egzekwować to po prostu z dużym ciągłym przydziałem.
Kultura i oczekiwania HPC
Programiści HPC po prostu oczekują danych niskiego poziomu. Jeśli dasz im matrycę, oczekuje się, że jeśli złapią wskaźnik do pierwszego elementu macierzy i wskaźnik do ostatniego elementu macierzy, wówczas wszystkie wskaźniki pomiędzy tymi dwoma są prawidłowe i wskazują na elementy tego samego matryca. Jest to podobne do mojego pierwszego punktu, ale inne, ponieważ może nie być tak bardzo powiązane z bibliotekami, ale raczej z członkami zespołu lub kimkolwiek, komu udostępniasz swój kod.
Łatwiej jest uzasadnić wydajność danych niższego poziomu
Zrzucenie na najniższy poziom reprezentacji pożądanej struktury danych ułatwia życie na dłuższą metę dla HPC. Korzystanie z narzędzi takich jak perfi vtunezapewni ci bardzo niski poziom liczników wydajności, które spróbujesz połączyć z tradycyjnymi wynikami profilowania w celu poprawy wydajności twojego kodu. Jeśli struktura danych wykorzystuje wiele fantazyjnych kontenerów, trudno będzie zrozumieć, że brak pamięci podręcznej wynika z problemu z kontenerem lub z nieefektywności samego algorytmu. W przypadku bardziej skomplikowanych kodów potrzebne są pojemniki, ale w przypadku algebry macierzowej tak naprawdę nie są - możesz sobie radzić z 1std::vectorprzechowywaniem danych zamiast nstd::vectors, więc idź z tym.
Piszę również punkt odniesienia. W przypadku matrycy o małym rozmiarze (<100 * 100) wydajność jest podobna dla wektora <wektor <podwójny >> i owiniętego wektora 1D. W przypadku matrycy o dużych rozmiarach (~ 1000 * 1000) lepiej jest owinięty wektor 1D. Macierz własna zachowuje się gorzej. Dziwi mnie, że Eigen jest najgorszy.
Jak zauważyli inni, nie próbuj z tym robić matematyki ani nie rób nic wydajnego.
To powiedziawszy, użyłem tej struktury jako tymczasowej, gdy kod musi złożyć tablicę 2-D, której wymiary zostaną określone w czasie wykonywania i po rozpoczęciu przechowywania danych. Na przykład, zbieranie danych wyjściowych wektora z jakiegoś kosztownego procesu, w którym nie jest łatwo dokładnie obliczyć, ile wektorów trzeba przechowywać przy starcie.
Możesz po prostu połączyć wszystkie swoje dane wektorowe w jednym buforze, gdy tylko się pojawią, ale kod będzie bardziej trwały i bardziej czytelny, jeśli użyjesz vector<vector<T>>.
Odpowiedzi:
To zły pomysł, ponieważ wektor musi przydzielić tyle obiektów w przestrzeni, ile macierzy w macierzy. Alokacja jest droga, ale przede wszystkim jest to zły pomysł, ponieważ dane macierzy istnieją teraz w wielu tablicach rozrzuconych wokół pamięci, a nie w jednym miejscu, w którym pamięć podręczna procesora może z łatwością uzyskać do niej dostęp.
Jest to również marnotrawny format przechowywania: std :: vector przechowuje dwa wskaźniki, jeden na początku tablicy i jeden na końcu, ponieważ długość tablicy jest elastyczna. Z drugiej strony, aby była to właściwa matryca, długości wszystkich rzędów muszą być takie same, więc wystarczyłoby przechować liczbę kolumn tylko raz, zamiast pozwolić, aby każdy wiersz przechowywał swoją długość niezależnie.
źródło
std::vector
przechowuje trzy wskaźniki: początek, koniec i koniec przydzielonego obszaru pamięci (na przykład pozwalając nam zadzwonić.capacity()
). Ta pojemność może różnić się od wielkości, co znacznie pogorszy sytuację!Oprócz powodów, o których wspomniał Wolfgang, jeśli używasz
vector<vector<double> >
, będziesz musiał wyrejestrować go dwukrotnie za każdym razem, gdy chcesz odzyskać element, co jest bardziej kosztowne obliczeniowo niż pojedyncza operacja dereferencji. Jednym typowym podejściem jest zamiast tego przydzielenie jednej tablicy (avector<double>
lub adouble *
). Widziałem także, jak ludzie dodają cukier składniowy do klas macierzy, owijając wokół tej pojedynczej tablicy kilka bardziej intuicyjnych operacji indeksowania, aby zmniejszyć ilość „mentalnego obciążenia” potrzebnego do wywołania właściwych wskaźników.źródło
Nie, użyj jednej z dostępnych bibliotek algebry liniowej. Dyskusję na temat różnych bibliotek można znaleźć tutaj: Zalecenia dotyczące użytecznej, szybkiej biblioteki macierzy C ++?
źródło
Czy to naprawdę takie złe?
@Wolfgang: W zależności od wielkości gęstej matrycy dwa dodatkowe wskaźniki na wiersz mogą być nieistotne. Jeśli chodzi o rozproszone dane, można pomyśleć o użyciu niestandardowego alokatora, który zapewnia, że wektory są w ciągłej pamięci. Tak długo, jak pamięć nie zostanie poddana recyklingowi, nawet standardowy alokator będzie nam przylegał do pamięci z odstępem o wielkości dwóch wskaźników.
@Geoff: Jeśli korzystasz z dostępu losowego i używasz tylko jednej tablicy, nadal musisz obliczyć indeks. Nie może być szybciej.
Zróbmy więc mały test:
vectormatrix.cc:
A teraz za pomocą jednej tablicy:
arraymatrix.cc
W moim systemie jest teraz wyraźny zwycięzca (kompilator gcc 4.7 z -O3)
odbitki vectormatrix czasu:
Widzimy również, że dopóki standardowy alokator nie przetwarza zwolnionej pamięci, dane są ciągłe. (Oczywiście po niektórych zwolnieniach nie ma na to gwarancji.)
odbitki macierzy czasu:
źródło
Nie polecam tego, ale nie z powodu problemów z wydajnością. Będzie on nieco mniej wydajny niż tradycyjna macierz, która zwykle jest alokowana jako duża część ciągłych danych, które są indeksowane za pomocą pojedynczego wskaźnika dereferencji i arytmetyki liczb całkowitych. Powodem spadku wydajności są głównie różnice w buforowaniu, ale gdy rozmiar macierzy wystarczająco się powiększy, efekt zostanie amortyzowany, a jeśli użyjesz specjalnego alokatora dla wewnętrznych wektorów, aby były one wyrównane do granic pamięci podręcznej, to dodatkowo złagodzi problem buforowania .
Moim zdaniem nie jest to wystarczający powód, aby tego nie robić. Powodem dla mnie jest to, że powoduje to wiele bólów głowy związanych z kodowaniem. Oto lista bólów głowy, które spowodują w dłuższej perspektywie
Korzystanie z bibliotek HPC
Jeśli chcesz korzystać z większości bibliotek HPC, musisz iterować wektor i umieścić wszystkie ich dane w ciągłym buforze, ponieważ większość bibliotek HPC oczekuje tego jawnego formatu. Przychodzą mi na myśl BLAS i LAPACK, ale także wszechobecna biblioteka HPC MPI byłaby znacznie trudniejsza w użyciu.
Większy potencjał błędu kodowania
std::vector
nic nie wie o swoich wpisach. Jeśli wypełniszstd::vector
więcejstd::vector
s, Twoim zadaniem jest upewnić się, że wszystkie mają ten sam rozmiar, ponieważ pamiętaj, że chcemy, aby macierz i macierze nie miały zmiennej liczby wierszy (lub kolumn). Dlatego będziesz musiał wywoływać wszystkie poprawne konstruktory dla każdego wpisu zewnętrznego wektora, a każdy, kto używa twojego kodu, musi oprzeć się pokusie użyciastd::vector<T>::push_back()
dowolnego z wewnętrznych wektorów, co spowodowałoby uszkodzenie całego następującego kodu. Oczywiście możesz tego zabronić, jeśli piszesz poprawnie swoją klasę, ale o wiele łatwiej jest egzekwować to po prostu z dużym ciągłym przydziałem.Kultura i oczekiwania HPC
Programiści HPC po prostu oczekują danych niskiego poziomu. Jeśli dasz im matrycę, oczekuje się, że jeśli złapią wskaźnik do pierwszego elementu macierzy i wskaźnik do ostatniego elementu macierzy, wówczas wszystkie wskaźniki pomiędzy tymi dwoma są prawidłowe i wskazują na elementy tego samego matryca. Jest to podobne do mojego pierwszego punktu, ale inne, ponieważ może nie być tak bardzo powiązane z bibliotekami, ale raczej z członkami zespołu lub kimkolwiek, komu udostępniasz swój kod.
Łatwiej jest uzasadnić wydajność danych niższego poziomu
Zrzucenie na najniższy poziom reprezentacji pożądanej struktury danych ułatwia życie na dłuższą metę dla HPC. Korzystanie z narzędzi takich jak
perf
ivtune
zapewni ci bardzo niski poziom liczników wydajności, które spróbujesz połączyć z tradycyjnymi wynikami profilowania w celu poprawy wydajności twojego kodu. Jeśli struktura danych wykorzystuje wiele fantazyjnych kontenerów, trudno będzie zrozumieć, że brak pamięci podręcznej wynika z problemu z kontenerem lub z nieefektywności samego algorytmu. W przypadku bardziej skomplikowanych kodów potrzebne są pojemniki, ale w przypadku algebry macierzowej tak naprawdę nie są - możesz sobie radzić z1
std::vector
przechowywaniem danych zamiastn
std::vector
s, więc idź z tym.źródło
Piszę również punkt odniesienia. W przypadku matrycy o małym rozmiarze (<100 * 100) wydajność jest podobna dla wektora <wektor <podwójny >> i owiniętego wektora 1D. W przypadku matrycy o dużych rozmiarach (~ 1000 * 1000) lepiej jest owinięty wektor 1D. Macierz własna zachowuje się gorzej. Dziwi mnie, że Eigen jest najgorszy.
źródło
Jak zauważyli inni, nie próbuj z tym robić matematyki ani nie rób nic wydajnego.
To powiedziawszy, użyłem tej struktury jako tymczasowej, gdy kod musi złożyć tablicę 2-D, której wymiary zostaną określone w czasie wykonywania i po rozpoczęciu przechowywania danych. Na przykład, zbieranie danych wyjściowych wektora z jakiegoś kosztownego procesu, w którym nie jest łatwo dokładnie obliczyć, ile wektorów trzeba przechowywać przy starcie.
Możesz po prostu połączyć wszystkie swoje dane wektorowe w jednym buforze, gdy tylko się pojawią, ale kod będzie bardziej trwały i bardziej czytelny, jeśli użyjesz
vector<vector<T>>
.źródło