Panel użytkownika
Nazwa użytkownika:
Hasło:
Nie masz jeszcze konta?

Optymalizacja pamięci, memory alignment

Ostatnio zmodyfikowano 2017-05-03 15:39
Autor Wiadomość
Sztywny
Temat założony przez niniejszego użytkownika
Optymalizacja pamięci, memory alignment
» 2017-05-02 14:53:29
Witam Panowie
Pozwolę sobie wkleić treść posta, który napisałem na innym forum, lecz nie uzyskałem użytecznych informacji w temacie:

Jak to jest z tym wyrównywaniem pamięci?
Czy daje to jakiś zauważalny wzrost wydajności? Jest to zależne od architektury procesora (lub/i systemu operacyjnego, proszę poprawić jeśli się mylę), bo np strukturę wyrównujemy pod konkretną długość słowa procesora danej architektury. Więc pisząc program, zawężam jego optymalne wykonanie do tej jednej konkretnej architektury? Podczas gdy ten sam program wykonywany na innym procesorze może mieć gorszą wydajność.

Czy jest to gra warta świeczki?
Czy to nie jest tak, że gdy kompiluję program pod daną architekturę to kompilator nie dokonuje własnych optymalizacji?
Jeśli tak, to jak duża jest przydatność narzędzi, które standard udostępnia, w tej kwestii? Oczywiście mówimy o nowoczesnych kompilatorach: clang, gcc, msvc, które potrafią efektywnie optymalizować kod.
A więc narzędzia udostępniane przez standard mogą być użyteczne jedynie w specjalnych kompilatorach, które spełniają jedynie absolutne minimum standardu.
Czy dobrze myślę?

znalazłem kilka użytecznych linków na stackoverflow, ale nie poruszają one istotnych zagadnień:
http://stackoverflow.com/questions/381244/purpose-of-memory-alignment
http://stackoverflow.com/questions/20809721/does-alignment-really-matter-for-performance-in-c11

Jak to się ma do pisania bibliotek?
Przecież nie wiem pod jaką architekturą będzie używany pisany kod, więc jak to wyrównywać? Nie obejdzie się bez pisania dyrektyw warunkowych preprocesora czy można to obskoczyć w inny, bardziej elegancki sposób?
Standard nie definiuje mnóstwa rzeczy, np wielkości typów wbudowanych i wskaźników są zależne od implementacji. Czy to ma jakiś większy związek z optymalizacją i wyrównywaniem pamięci?

Dodatkowo jeszcze jest sprawa z porządkowaniem pól w strukturach. Tzn, deklaracja od największego rozmiaru do najmniejszego, powoduje zaoszczędzenie miejsca. Jednak czy zysk z takich działań jest wart pogorszenia czytelności i zaburzenia porządku klauzul dostępu(public, private, protected)?
P-160656
DejaVu
» 2017-05-02 21:23:16
Mikro optymalizacje nie są opłacalne. W praktyce nikt się nimi nie zajmuje jeżeli rozmawiamy o produktach komercyjnych, które nie dotykają bezpośrednio sterowników sprzętowych.

Podstawowe zasady optymalizacji:
- nie optymalizuj przedwcześnie (dopóki nie masz dowodów, że coś jest lub będzie wolne to nie optymalizuj)
- nie wykonuj mikro-optymalizacji (dużo ważniejsze jest, aby kod był prosty i czytelny aniżeli zawierał wyrafinowaną optymalizację, która i tak nie wiadomo czy faktycznie daje korzyści)
- pozwól, aby kompilator optymalizował instrukcje jeżeli sam uzna, że to się opłaca (nie baw się w Assemblera i inne tego typu rzeczy - Twoja optymalizacja będzie ukierunkowana tylko i wyłącznie na sprzęt jaki posiadasz - inny procesor może charakteryzować się nieco innymi czasami wykonywania poszczególnych instrukcji).
- chcesz optymalizować - optymalizuj złożoność obliczeniową algorytmów, a nie poszczególne linijki w kodzie

Czytelność kodu jest nieporównywalnie ważniejsza niż kilka procent więcej na wydajności algorytmu. Wydajność na poziomie pojedynczych instrukcji lub na poziomie wyrównywania pamięci ma znaczenie dopiero wtedy, gdy jest to sterownik do sprzętu i producent chce, aby jego sprzęt wypadał jak najlepiej na tle konkurencji. Wówczas firma zatrudnia sztab osób, które się specjalizują w mikro-optymalizacjach i w tym, aby rozkładać dane w pamięci maksymalnie korzystnie dla CPU. Warto w tym miejscu dodać, że sterowniki są zazwyczaj małe, więc mało jest kodu do optymalizowania i nawet jeżeli będzie on mało czytelny to i tak w razie problemów jesteś w stanie go szybko napisać od nowa (w najgorszym przypadku). W przypadku aplikacji dla użytkownika kodu jest znacznie więcej i tym samym kluczowym jest pisać kod w sposób czysty, prosty, czytelny i tak, aby kod był samo dokumentujący się (czytaj: do zrozumienia ekranu kodu powinno Ci wystarczyć ~10-15 sekund).
P-160668
Sztywny
Temat założony przez niniejszego użytkownika
» 2017-05-02 23:13:15
Niby tak, ale optymalizacja pojedynczych linijek, wydaje się również mieć sens. Tzn mam na myśli ładowanie słowa const i korzystanie z obiektów const wszędzie gdzie się tylko da. Używanie słowa noexcept i constexpr, również daje zysk, nic nie kosztuje i nie pogarsza czytelności kodu. Tak samo przedkładanie wyrażeń lambda nad std::bind, czy przenoszenie zamiast kopiowania.

Powiedz mi jeszcze proszę, jak wyglądają optymalizacje programów wielowątkowych? Tzn czy korzystać z wysokopoziomowych rozwiązań, typu std::async, std::future, std::promise, std::lock_quard, etc...
Czy może zejść niskopoziomowo i porządkować dostęp do pamięci przy użyciu typów atomowych?
Problem jest niestety taki, że standard gwarantuje atomowe wywołanie tylko dla jednego typu, reszta w zależności od implementacji może być wywołana z użyciem wewnętrznych blokad (czyli po prostu mutexu).
Kiedyś chciałem zmierzyć szybkość takiego kodu, ale nie miało to sensu, ponieważ czasy wywołania określonych funkcji miały za dużo odchyleń by można było coś z tego wywnioskować.
Tak więc czy to ma sens? Jak duży skok wydajności daje korzystanie z rozwiązań atomowych by opłacalne było porzucenie prostego mutexu i funkcji async?

Co mógłbyś mi powiedzieć na temat pisania własnych alokatorów, przeciążania new i delete tak by korzystały z pul pamięci? Kiedyś mierzyłem czasy alokacji, przy użyciu operatora new, malloc i alokacji na pulach z boosta. Alokacja na pulach była kilka tysięcy razy szybsza. Z tego co wiem, w gperftools, są również bardzo wydajne pule, ale nie miałem okazji z nich korzystać.
Korzystanie z boosta jest o tyle wygodne iż w większości firm jest traktowany on prawie na równi ze standardem i można z niego korzystać bez większych przeciwwskazań.
P-160672
DejaVu
» 2017-05-03 00:04:40
Alokacja na pulach była kilka tysięcy razy szybsza.
To nie jest realne ani możliwe. Sam pisałem alokatory i dało się osiągnąć 10-krotnie szybszą alokację pamięci np. dla listy. Przykład:
http://cpp0x.pl/forum/temat/​?id=4004

W praktyce nie korzystam z własnych alokatorów, ponieważ nie ma nigdzie uzasadnienia wydajnościowego. W praktyce też napisałem całą masę różnego kodu (w tym wielowątkowego) i szczerze powiem Ci, że należy korzystać z tych narzędzi, które są najprostsze w użyciu i najlepiej niezależne od platformy. Przykładowo ja używam std::thread, std::lock_guard, std::mutex i miejscami również std::atomic. Nie ugrasz nic na wydajności jeżeli będziesz miał całą masę synchronizacji między wątkami. Jak chcesz z wielowątkowości maksymalnie dużo wycisnąć to powinieneś wątkom zlecać jakieś zadania do przeliczenia, które nie wymagają między sobą synchronizacji w trakcie wykonywania obliczeń.

Wydajność to coś na co nie trzeba w obecnych czasach kłaść dużego nacisku. Potrzebny jest zdrowy rozsądek. Znajdź najpierw realny zbiór danych dla którego Twój algorytm będzie wolny. Jak taki zbiór danych znajdziesz to dopiero możesz zacząć myśleć o jakiejkolwiek optymalizacji. Const, constexpr nie ma większego znaczenia w dużych aplikacjach, ponieważ w dużych aplikacjach to nie stałe decydują o jej szybkości tylko cały kod dookoła. Już nie raz spotkałem się, że w produkcyjnych aplikacjach widywałem samodzielną implementację sortowania bąbelkowego, kopiowanie dużych obiektów zamiast przekazywanie ich przez referencję itd. Jak się robi podstawowe błędy to większa aplikacja nigdy nie będzie ani szybka ani wydajna. Podstawa to pisanie algorytmów o dobrej złożoności obliczeniowej i minimalizacja punktów synchronizacji wielu wątków.

/edit:
Osobiście nie używam boosta, a firma, która po drodze używała boosta zaczęła się z niego wycofywać. Sądzę, że standard C++ w obecnej postaci jest na tyle rozsądny, że korzyści z korzystania z boosta są raczej już marne. Są firmy i osoby, które z niego korzystają, ale sądzę, że w nowych aplikacjach unikałbym w ogóle boosta, ponieważ standard C++17 ma po prostu sporo gotowych rozwiązań i nie wymaga to wciągania przeszło 100MB kodu źródłowego.
P-160674
Elaine
» 2017-05-03 06:15:05
tl;dr: wyrównanie nie ma żadnego związku z optymalizacją

Jak to jest z tym wyrównywaniem pamięci?
Na niektórych architekturach niewyrównane dostępy do pamięci powodują wyjątek procesora, który w typowych systemach operacyjnych powoduje natychmiastowe ubicie procesu (chyba że proces ma własną obsługę SIGBUS (POSIX), STATUS_DATATYPE_MISALIGNMENT (Windows), itp.). Na innych niewyrównane dostępy mogą wykonywać się normalnie, ale wolniej. Z tego powodu standard definiuje dla każdego typu wyrównanie, czyli potęgę dwójki, której wielokrotnościami są adresy wszystkich obiektów danego typu.

W normalnych warunkach — czyli bez grzebania z gołą pamięcią — wyrównaniem zajmują się kompilator i biblioteka standardowa. Zadbają one o to, by gdy programista napisze int foo lub std::vector<int> bar, te inty zostały stworzone pod adresami podzielnymi przez alignof(int). Programista nie musi się tutaj przejmować niczym. Dotyczy się to zarówno "zwykłych" zmiennych, jak i pól w strukturach: na przykład, na typowych architekturach, w struct foo { char a; int b; }; kompilator sam wstawi trzy bajty wyrównania między pola, by pole typu int było wyrównane do 4 bajtów.

Sytuacje, gdy obowiązek zadbania o wyrównanie pamięci spada na programistę są rzadkie, i sprowadzają się głównie do tworzenia obiektów przy pomocy placement new w gołych tablicach bajtów. Jeśli programista robi takie rzeczy — a rzadko zachodzi potrzeba, bo implementacje dwóch głównych zastosowań tej techniki, czyli boost::optional i boost::variant, już istnieją — to najczęściej wystarczy dobrze umiejscowione alignas(T).


Oczywiście, czasami może zajść potrzeba zminimalizowania liczby bajtów straconych na padding w strukturze. Wtedy wystarczy ułożyć pola zaczynając od tych o największym wyrównaniu, a kończąc na tych o najmniejszym. Należy zauważyć, że mowa tutaj o wyrównaniu, nie rozmiarze. Jeśli mamy taką struktrę: struct foo { char a[15]; long b; char c; };, to, na typowej maszynie, największym polem jest a, ale największe wyrównanie ma b! Na moim komputerze ta struktura ma 32 bajty, a po przesunięciu b na początek, ma 24.



Czy to nie jest tak, że gdy kompiluję program pod daną architekturę to kompilator nie dokonuje własnych optymalizacji?
Standard nie pozwala kompilatorom zmienić kolejności pól (co jest chyba jedyną optymalizacją mającą związek z wyrównaniem), chyba że pola są w różnych sekcjach dostępu – na przykład w struct foo { public: int a; public int b; }; nie ma żadnej gwarancji, czy pierwsze w pamięci będzie pole a czy b — ale żaden znany mi kompilator nawet wtedy tego nie robi.

Przecież nie wiem pod jaką architekturą będzie używany pisany kod, więc jak to wyrównywać? Nie obejdzie się bez pisania dyrektyw warunkowych preprocesora czy można to obskoczyć w inny, bardziej elegancki sposób?
Możesz po prostu nie robić nic, i w 99% przypadków to wystarczy. W pozostałym 1% możesz przyjąć, że, jeśli chodzi o wyrównanie, long double > double i long long > wskaźniki i long > float i int > short > bool i char. Nie potrafię sobie przypomnieć żadnej architektury, gdzie stosowanie takiej reguły kciuka byłoby złym pomysłem.

Jednak czy zysk z takich działań jest wart pogorszenia czytelności
Pogorszenia czytelności? Czy zmiana kolejności pól naprawdę ma wielki wpływ na czytelność?
i zaburzenia porządku klauzul dostępu(public, private, protected)?
Zwykle pola są albo wszystkie publiczne, albo wszystkie prywatne.



Oczywiście mówimy o nowoczesnych kompilatorach: clang, gcc, msvc, które potrafią efektywnie optymalizować kod.
Heh. Nie raz widziałem, jak któryś z tych kompilatorów poległ na wydawałoby się prostym kodzie.
P-160677
Sztywny
Temat założony przez niniejszego użytkownika
» 2017-05-03 15:16:47
Super Panowie,
dzięki za naprawdę merytoryczne wypowiedzi.

Co do standardu c++17 to aktualnie żaden z kompilatorów (http://en.cppreference.com/w/cpp/compiler_support) nie wspiera go w 100%, tak więc jeszcze przyjdzie trochę nam poczekać. Tym bardziej, że oficjalne "wypuszczenie" standardu miało miejsce miesiąc temu.
Boost jest świetnym rozwiązaniem, ponieważ możemy skorzystać z rozwiązań, które do standardu trafiłyby (jeśli w ogóle) za jakiś czas, np boost.asio, etc..

#edit
Poniosło mnie z tymi kilkoma tysiącami.... :d
Mierzyłem to w ten sposób:

template< class Clock, class Precision, class TimeWrapper, class Func, class... Args>
auto measure(std::reference_wrapper< TimeWrapper > tWrapper, Func&& func, Args&&... args)
{
auto  t1 = Clock::now();
auto result = std::forward< Func >(func)(std::forward< Args >(args)...);
auto t2 = Clock::now();

tWrapper.get() = std::chrono::duration_cast< Precision >(t2 - t1);
return result;
}
 
Wiem, że nie jest to bezpieczne, ale nie chciało mi się bawić z wywołaniami std::enable_if. Poza tym to tylko dla testów, to nie kod produkcyjny.

Mierzone funkcje:

int allocationBoostPool() {
{
boost::pool<> memoryPool(sizeof(int));
for (size_t i = 0; i < VALUE_OF_OBJECT; i++) {
int* ptr = reinterpret_cast<int*>(memoryPool.malloc());
}

}
return 0;

}
int allocationNewOperator() {
for(size_t i =0; i<VALUE_OF_OBJECT; i++) {
int* ptr = new int;
delete ptr;
}
return 0;
}
int allocationMallocOper() {
for(size_t i=0; i<VALUE_OF_OBJECT; i++) {
int* ptr = reinterpret_cast<int*>(std::malloc(sizeof(VALUE_OF_OBJECT)));
std::free(ptr);
}
return 0;
}
 

oraz przykładowy pomiar:

auto funcObj = []() ->int { return allocationBoostPool(); };
std::chrono::duration<long long, std::nano> timeWrap;
auto result = measure<std::chrono::high_resolution_clock, std::chrono::nanoseconds>(std::ref(timeWrap), funcObj);
std::cout << result << ' ' << timeWrap.count() << '\n';
 

Zawsze wychodziło, że alokacja na pulach z boosta była szybsza o wiele tysięcy nanosekund, stąd moja pomyłka :D

down:
No fakt gcc 7.1 wypuścili wczoraj :)
P-160684
Elaine
» 2017-05-03 15:39:47
Co do standardu c++17 to aktualnie żaden z kompilatorów […] nie wspiera go w 100%
GCC 7 ma pełne wsparcie dla C++17, jeśli chodzi o sam kompilator. Biblioteka standardowa, czyli libstdc++, jest odrobinę do tyłu i brakuje kilku rzeczy. Zawsze można użyć innej, na przykład libc++, ale ta obecnie też jest do tyłu.

Boost jest świetnym rozwiązaniem, ponieważ możemy skorzystać z rozwiązań, które do standardu trafiłyby (jeśli w ogóle) za jakiś czas, np boost.asio, etc..
Nawet biblioteki Boosta zaadoptowane już do standardu często są zwyczajnie bardziej użyteczne w Booście niż w bibliotece standardowej. Na przykład boost::future::then istnieje od czterech lat, std::future::then nie ma nawet w C++17, a rozszerzeń dla współbieżności i std::experimental::future::then nie implementuje obecnie chyba nic.
P-160685
« 1 »
  Strona 1 z 1