C++ : optymalizacja kodu / Kurt Guntheroth. – Warszawa, 2016 Spis
Transkrypt
C++ : optymalizacja kodu / Kurt Guntheroth. – Warszawa, 2016 Spis
C++ : optymalizacja kodu / Kurt Guntheroth. – Warszawa, 2016 Spis treści Przedmowa xv 1 Wprowadzenie do optymalizacji Optymalizacja to część procesu rozwoju oprogramowania Optymalizacja jest efektywna Optymalizacja jest OK Nanosekunda tu, nanosekunda tam Podsumowanie strategii optymalizacji kodu C++ Użyj lepszego kompilatora, lepiej używaj kompilatora Użyj lepszych algorytmów Użyj lepszych bibliotek Zredukuj alokację pamięci i kopiowanie Usuń obliczenia Użyj lepszych struktur danych Zwiększ równoległość Zoptymalizuj zarządzanie pamięcią Podsumowanie 1 2 3 4 6 7 7 9 10 11 12 12 13 13 13 2 Wpływ działania komputera na optymalizację Nieprawdziwe przekonania języka C++ o komputerach Prawda o komputerach Pamięć jest powolna Dostęp do pamięci nie zamyka się w bajtach Nie wszystkie operacje dostępu do pamięci są równie wolne Słowa mają najbardziej i najmniej znaczący koniec Pamięć ma ograniczoną pojemność Wykonanie instrukcji zabiera dużo czasu Komputery mają trudności z podejmowaniem decyzji Istnieje wiele strumieni wykonania programu Wywoływanie systemu operacyjnego jest kosztowne C++ również kłamie Różne instrukcje mają różny koszt Instrukcje nie są wykonywane kolejno Podsumowanie 15 16 17 17 18 19 20 21 21 22 22 24 24 25 25 26 3 Mierzenie wydajnośc i Mentalność optymalizatora Wydajność musi być mierzona Optymalizatorzy polują na grubą zwierzynę 27 28 28 29 Reguła 90/10 Prawo Amdahla Przeprowadzanie eksperymentów Prowadź notatnik laboratoryjny Mierzenie bazowej wydajności i wyznaczanie celów Można poprawić tylko to, co zostało zmierzone Działanie programu profilującego Pomiary czasowe długotrwałych zadań „Odrobina wiedzy" o mierzeniu czasu Mierzenie czasu przy użyciu komputerów Pokonywanie trudności w mierzeniu Tworzenie klasy stopera Mierzenie czasu aktywnych funkcji w warunkach testowych Szacowanie kosztu kodu w celu znalezienia aktywnego kodu Szacowanie kosztu pojedynczych instrukcji C++ Szacowanie kosztu pętli Inne sposoby odnajdowania aktywnych punktów Podsumowanie 29 31 32 34 35 37 37 40 41 46 55 58 63 64 64 65 67 68 4 Optymalizowanie użycia ciągów: przykład Dlaczego ciągi są tak problematyczne Ciągi są dynamicznie alokowane Ciągi to wartości Ciągi wymagają wiele kopiowania Pierwsze podejście do optymalizacji ciągów Użyj operacji mutujących do eliminowania wartości tymczasowych Redukuj realokację poprzez rezerwację obszaru Eliminuj kopiowanie argumentów std::string Eliminuj wyłuskania wskaźników przy użyciu iteratorów Eliminuj kopiowanie wartości zwrotnych Użyj tablic znaków zamiast ciągów Podsumowanie pierwszego podejścia do optymalizacji Drugie podejście do optymalizacji ciągów Użyj lepszego algorytmu Użyj lepszego kompilatora Użyj lepszej biblioteki ciągów Użyj lepszego alokatora Eliminowanie konwersji ciągów Konwersja z ciągu С do std::string Konwersja między kodowaniami znaków Podsumowanie 69 69 70 71 71 73 74 75 75 77 78 79 81 81 81 84 84 88 90 90 91 91 5 Optymalizowanie algorytmów Koszt czasowy algorytmów Koszt czasowy w najlepszym, średnim i najgorszym przypadku 93 95 97 Amortyzowany koszt czasowy Inne koszty Zestaw narzędzi do optymalizacji wyszukiwania i sortowania Efektywne algorytmy wyszukiwania Koszt czasowy algorytmów wyszukiwania Wszystkie wyszukania są równie dobre dla małych n Efektywne algorytmy sortowania Koszt czasowy algorytmów sortowania Zastąpienie algorytmów sortowania o niewydajnym najgorszym przypadku Eksploatowanie znanych właściwości danych wejściowych Wzorce optymalizacji Wstępne obliczanie Opóźnione obliczanie Przetwarzanie wsadowe Buforowanie Specjalizacja Pobieranie większych porcji Wskazówki Optymalizowanie oczekiwanej ścieżki Mieszanie Podwójne sprawdzanie Podsumowanie 98 98 98 99 99 100 101 102 6 Optymalizacja zmiennych dynamicznych Powtórzenie wiadomości o zmiennych C++ Czas magazynowania Własność zmiennych Obiekty wartości i obiekty encji Powtórzenie wiadomości o API zmiennych dynamicznych w C++ Sprytne wskaźniki automatyzują zarządzanie własnością zmiennych dynamicznych Dynamiczne zmienne wpływają na koszt czasowy wykonania Redukowanie użycia zmiennych dynamicznych Twórz instancje klas statycznie Używaj statycznych struktur danych Użyj std::make_shared zamiast new Nie dziel się własnością niepotrzebnie Użyj „głównego wskaźnika" jako właściciela zmiennych dynamicznych Redukowanie realokacji zmiennych dynamicznych Wstępnie alokuj zmienne dynamiczne, aby zapobiec realokacji Twórz zmienne dynamiczne poza pętlą Eliminowanie niepotrzebnego kopiowania Wyłącz nieumyślne kopiowanie w definicji klasy Wyeliminuj kopiowanie podczas wywoływania funkcji 111 112 112 115 116 117 102 103 103 104 105 106 106 107 107 108 108 108 109 109 120 123 124 124 125 129 130 131 132 132 133 134 135 136 Wyeliminuj kopiowanie podczas powrotów z funkcji Biblioteki bez kopiowania Implementuj idiom „kopiowanie przy zapisie" Stosuj wycinki struktur danych Implementowanie semantyki przenoszenia Niestandardowa semantyka kopiowania: desperackie rozwiązanie std::swap(): semantyka przenoszenia dla ubogich Współwłasność encji Przenoszone części semantyki przenoszenia Uaktualnij kod w celu użycia semantyki przenoszenia Subtelności semantyki przenoszenia Spłaszczanie struktur danych Podsumowanie 138 140 141 142 143 143 144 145 146 148 148 151 152 7 Optymalizowanie aktywnych instrukcji Usuwanie kodu z pętli Buforuj wartość końcową pętli Użyj bardziej efektywnych instrukcji pętli Odliczaj w dół zamiast w górę Usuń z pętli niezależny kod Usuń z pętli niepotrzebne wywołania funkcji Usuń z pętli ukryte wywołania funkcji Usuń z pętli kosztowne, wolno zmieniające się wywołania Przesuń pętle do funkcji, aby zredukować dodatkowy koszt wywołań Rzadziej wykonuj niektóre akcje A co z całą resztą? Usuwanie kodu z funkcji Koszt wywołań funkcji Deklaruj krótkie funkcje jako inline Definiuj funkcje przed pierwszym użyciem Eliminuj niepotrzebny polimorfizm Usuń nieużywane interfejsy Wybieraj implementację w czasie kompilacji przy użyciu szablonów Eliminuj zastosowania idiomu PIMPL Eliminuj wywołania bibliotek DLL Użyj statycznych funkcji składowych zamiast funkcji składowych Przenieś wirtualny destruktor do klasy podstawowej Optymalizowanie wyrażeń Uprość wyrażenia Grupowanie stałych Użyj mniej kosztownych operatorów Używaj arytmetyki liczb całkowitych zamiast arytmetyki liczb zmiennoprzecinkowych Typ double może być szybszy niż float Zastąp obliczenia iteracyjne wzorami 153 154 155 155 156 157 158 161 163 164 165 166 167 167 171 172 172 173 177 178 180 180 181 182 182 184 184 185 187 187 Optymalizacja idiomów przepływu sterowania Użyj switch zamiast if - else if - else Użyj funkcji wirtualnych zamiast switch lub if Korzystaj z bezkosztowej obsługi wyjątków Podsumowanie 189 189 190 191 193 8 Zastosowanie lepszych bibliotek Optymalizacja użycia biblioteki standardowej Filozofia standardowej biblioteki C++ Problemy ze stosowaniem standardowej biblioteki C++ Optymalizowanie istniejących bibliotek Zmieniaj tak mało, jak to tylko możliwe Dodawaj funkcje zamiast zmieniać funkcjonalność Projektowanie zoptymalizowanych bibliotek Koduj szybko, ubolewaj w czasie wolnym W projektowaniu bibliotek ascetyzm to zaleta Podejmuj decyzje o alokacji pamięci poza biblioteką Programuj szybkie biblioteki Łatwiej jest optymalizować funkcje niż framework Spłaszcz hierarchie dziedziczenia Spłaszcz łańcuchy wywołań Spłaszcz wielowarstwowe projekty Unikaj dynamicznego wyszukiwania Wystrzegaj się „wszechmocnych funkcji" Podsumowanie 195 195 196 197 199 200 200 201 201 202 203 203 204 204 205 205 206 207 209 9 Optymalizacja wyszukiwania i sortowania Tabele klucz/wartość wykorzystujące std::map i std::string Zestaw narzędzi do podnoszenia wydajności wyszukiwania Dokonywanie bazowego pomiaru Zidentyfikuj aktywność do zoptymalizowania Rozłóż aktywność do zoptymalizowania Zmodyfikuj lub zastąp algorytmy i struktury danych Przeprowadź proces optymalizacji na niestandardowych abstrakcjach Optymalizowanie wyszukiwania przy użyciu std::map Użyj tablic znaków o stałym rozmiarze w roli kluczy std::map Użyj std::map z kluczami ciągu w stylu języka С Użyj std::set dla kluczy zawartych w wartościach Optymalizowanie wyszukiwania przy użyciu nagłówka <algorithm> Przeszukiwana tabela klucz/wartość w kontenerach sekwencji std::find(): oczywista nazwa, koszt czasowy O(n) std::binary_search(): nie zwraca wartości Wyszukiwanie binarne przy użyciu std::equal_range() Wyszukiwanie binarne przy użyciu std::lower_bound() Własna implementacja wyszukiwania binarnego 211 212 213 214 215 215 216 218 219 219 220 223 224 225 226 227 228 228 229 Własna implementacja wyszukiwania binarnego przy użyciu strcmp() Optymalizowanie wyszukiwania w tabelach mieszania klucz/wartość Mieszanie przy użyciu std::unordered_map Mieszanie, gdy klucze są tablicami o stałej liczbie znaków Mieszanie, gdy klucze są ciągami zakończonymi znakiem nuli Mieszanie z niestandardową tabelą Konsekwencje abstrakcji Stepanova Optymalizuj sortowanie przy użyciu standardowej biblioteki C++ Podsumowanie 230 231 232 233 234 236 237 239 240 10 Optymalizowanie struktur danych Poznaj kontenery z biblioteki standardowej Kontenery sekwencji Kontenery asocjacyjne Eksperymentowanie z kontenerami biblioteki standardowej std::vector i std::string Wpływ realokacji na wydajność Wstawianie i usuwanie z std::vector Iterowanie po kontenerze std::vector Sortowanie kontenera std::vector Przeszukiwanie kontenera std::vector std::deque Wstawianie i usuwanie z std::deque Iterowanie po kontenerze std::deque Sortowanie kontenera std::deque Przeszukiwanie kontenera std::deque std::list Wstawianie i usuwanie z std::list Iterowanie po kontenerze std::list Sortowanie kontenera std::list Przeszukiwanie kontenera std::list std::forward_list Wstawianie i usuwanie z std::forward_list Iterowanie po kontenerze std::forward_list Sortowanie kontenera std::forward_list Przeszukiwanie kontenera std::forward_list std::map i std::multimap Wstawianie i usuwanie z std::map Iterowanie po kontenerze std::map Sortowanie kontenera std::map Przeszukiwanie kontenera std::map std::set i std::multiset std::unordered_map i std::unordered_multimap Wstawianie i usuwanie z std::unordered_map Iterowanie po kontenerze std::unordered_map 241 241 242 242 243 249 250 251 253 254 254 255 256 257 258 258 259 261 261 262 262 262 264 264 264 264 264 266 268 268 268 269 270 274 274 Przeszukiwanie kontenera std::unordered_map Inne struktury danych Podsumowanie 274 275 277 11 Optymalizowanie we/wy Przepis na odczytywanie plików Tworzenie ascetycznej sygnatury funkcji Skracanie łańcucha wywoływania Redukowanie realokacji Pobierz większe porcje - użyj większego bufora wejściowego Pobieraj większe porcje - odczytuj pojedyncze wiersze Ponowne skracanie łańcucha wywołań Zmiany, które nie pomogły Zapisywanie plików Odczytywanie z std::cin i zapisywanie w std::cout Podsumowanie 279 279 281 283 283 286 286 288 290 291 292 292 12 Optymalizowanie równoległości Powtórzenie informacji o równoległości Wprowadzenie do świata równoległości Wykonanie z przeplotem Spójność sekwencyjna Wyścigi Synchronizacja Atomowość Powtórzenie informacji o obsłudze równoległości w języku C++ Wątki Obietnice i przyszłości Zadania asynchroniczne Mutexy Blokady Zmienne warunkowe Atomowe operacje na współdzielonych zmiennych Plany: Przyszłość równoległości w języku C++ Optymalizowanie wielowątkowych programów C++ Wybieraj std::async zamiast std::thread Dopasuj liczbę wątków do liczby rdzeni Implementuj kolejkę zadań i pulę wątków Wykonywanie operacji we/wy w osobnym wątku Program bez synchronizacji Usuwanie kodu z uruchamiania i zamykania Zwiększenie wydajności synchronizacji Redukuj zakres sekcji krytycznych Ograniczaj liczbę równoległych wątków Unikaj problemu masowego pędu 293 294 295 299 300 301 302 303 305 305 307 309 311 312 313 316 319 321 321 323 325 326 326 329 330 330 331 332 Unikaj konwoju blokady Zredukuj rywalizację Nie oczekuj aktywnie w jednordzeniowym systemie Nie czekaj w nieskończoność Tworzenie własnego mutexu może być nieefektywne Ogranicz długość kolejki wyjściowej producenta Biblioteki wspierające równoległość Podsumowanie 333 333 335 335 335 336 336 338 13 Optymalizowanie zarządzania pamięcią Powtórzenie wiadomości o API zarządzania pamięcią w języku C++ Cykl życia zmiennych dynamicznych Funkcje zarządzania pamięcią alokują i zwalniają pamięć Wyrażenia new konstruują zmienne dynamiczne Wyrażenia delete usuwają zmienne dynamiczne Jawne wywołania destruktora usuwają zmienne dynamiczne Wysoko wydajne menedżery pamięci Dostarczanie menedżera pamięci specyficznego dla klasy Menedżer pamięci o stałym rozmiarze bloku Arena bloków Dodawanie specyficznego dla klasy operator new() Wydajność menedżera pamięci o stałym rozmiarze bloku Różne warianty menedżera pamięci o stałym rozmiarze bloku Menedżery pamięci nieobsługujące wielowątkowości są wydajne Dostarczanie niestandardowych alokatorów Minimalny alokator C++11 Dodatkowe definicje dla alokatora C++98 Alokator o stałym rozmiarze bloków Alokator o stałym rozmiarze bloków dla ciągów Podsumowanie 339 340 340 341 344 347 348 349 351 352 355 357 358 359 360 360 363 365 369 371 373 Indeks 375 O autorze 389 oprac. BPK