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