Udostępnij za pośrednictwem


Wskazówki dotyczące poprawiania kodu wrażliwego na czas

Pisanie szybkiego kodu wymaga zrozumienia wszystkich aspektów aplikacji i sposobu interakcji z systemem. Ten artykuł sugeruje alternatywy dla niektórych z bardziej oczywistych technik kodowania, aby ułatwić zapewnienie, że fragmenty kodu o krytycznym czasie działają w sposób zadowalający.

Podsumowując, poprawa kodu krytycznego czasowo wymaga:

  • Dowiedz się, które części programu muszą być szybkie.

  • Poznaj rozmiar i szybkość kodu.

  • Poznaj koszt nowych funkcji.

  • Poznaj minimalną pracę wymaganą do wykonania zadania.

Aby zebrać informacje na temat wydajności kodu, możesz użyć monitora wydajności (perfmon.exe).

Sekcje w tym artykule

Błędy pamięci podręcznej i błędy stron

Nieodebrane trafienia pamięci podręcznej zarówno w wewnętrznej, jak i zewnętrznej pamięci podręcznej, a także błędy stron (przejście do pomocniczego magazynu w celu uzyskania instrukcji i danych programu) spowalnia wydajność programu.

Trafienie pamięci podręcznej procesora CPU może kosztować program 10-20 cykli zegarowych. Trafienie zewnętrznej pamięci podręcznej może kosztować 20-40 cykli zegarowych. Błąd strony może kosztować milion cykli zegarowych (przy założeniu procesora obsługującego 500 milionów instrukcji/sekundy i czasu 2 milisekundy dla błędu strony). W związku z tym w najlepszym interesie wykonywania programu jest napisanie kodu, który zmniejszy liczbę nieodebranych trafień pamięci podręcznej i błędów stron.

Jednym z powodów powolnego podejmowania programów jest to, że częściej niż jest to konieczne, biorą więcej błędów stron lub pomijają pamięć podręczną. Aby uniknąć tego problemu, ważne jest, aby używać struktur danych z dobrą lokalnością odwołania, co oznacza utrzymywanie powiązanych elementów razem. Czasami struktura danych, która wygląda świetnie, okazuje się okropna ze względu na słabą lokalność odwołania, a czasami odwrotnie jest prawdziwa. Oto dwa przykłady:

  • Dynamicznie przydzielane połączone listy mogą zmniejszyć wydajność programu. Podczas wyszukiwania elementu lub przechodzenia przez listę na końcu każdy pominięty link może przegapić pamięć podręczną lub spowodować błąd strony. Implementacja listy oparta na prostych tablicach może być szybsza z powodu lepszego buforowania i mniejszej liczby błędów stron. Nawet jeśli pozwolisz na fakt, że tablica będzie trudniejsza do wzrostu, nadal może być szybsza.

  • Tabele skrótów używające dynamicznie przydzielonych połączonych list mogą obniżyć wydajność. Dzięki rozszerzeniu tabele skrótów używające dynamicznie przydzielonych połączonych list do przechowywania ich zawartości mogą znacznie gorzej działać. W ostatniej analizie proste wyszukiwanie liniowe za pośrednictwem tablicy może być rzeczywiście szybsze (w zależności od okoliczności). Użycie tabeli skrótów opartych na tablicy (tzw. "zamknięte tworzenie skrótów") jest często pomijaną implementacją, która często ma lepszą wydajność.

Sortowanie i wyszukiwanie

Sortowanie jest z natury czasochłonne w porównaniu z wieloma typowymi operacjami. Najlepszym sposobem uniknięcia niepotrzebnego spowolnienia jest unikanie sortowania w krytycznych momentach. Może być możliwe:

  • Odroczenie sortowania do czasu niekrytycznego dla wydajności.

  • Posortuj dane we wcześniejszym, krytycznym czasie.

  • Sortuj tylko część danych, które naprawdę wymagają sortowania.

Czasami można utworzyć listę w kolejności posortowanej. Należy zachować ostrożność, ponieważ jeśli musisz wstawić dane w kolejności posortowanej, może być wymagana bardziej skomplikowana struktura danych z niską lokalnością odwołania, co prowadzi do chybień pamięci podręcznej i błędów stron. Nie ma podejścia, które działa we wszystkich przypadkach. Wypróbuj kilka podejść i zmierz różnice.

Poniżej przedstawiono kilka ogólnych wskazówek dotyczących sortowania:

  • Użyj sortowania zapasów, aby zminimalizować błędy.

  • Każda praca, którą można wykonać wcześniej, aby zmniejszyć złożoność tego rodzaju, jest warta. Jeśli jednorazowe przekazywanie danych upraszcza porównania i zmniejsza sortowanie z O(n log n) do O(n), prawie na pewno wyjdziesz z przodu.

  • Pomyśl o lokalności odwołania do algorytmu sortowania i danych, na których oczekujesz, że będzie on uruchamiany.

Istnieje mniej alternatyw dla wyszukiwań niż sortowanie. Jeśli wyszukiwanie ma krytyczne znaczenie dla czasu, wyszukiwanie binarne lub wyszukiwanie tabeli skrótów jest prawie zawsze najlepsze, ale podobnie jak w przypadku sortowania, należy pamiętać o lokalności. Wyszukiwanie liniowe za pośrednictwem małej tablicy może być szybsze niż wyszukiwanie binarne za pośrednictwem struktury danych z wieloma wskaźnikami, które powodują błędy stron lub błędy pamięci podręcznej.

Biblioteki MFC i klas

Klasy programu Microsoft Foundation (MFC) znacznie upraszczają pisanie kodu. Podczas pisania kodu krytycznego dla czasu należy pamiętać o narzucie związanym z niektórymi klasami. Sprawdź kod MFC używany przez kod krytyczny czas, aby sprawdzić, czy spełnia wymagania dotyczące wydajności. Poniższa lista identyfikuje klasy i funkcje MFC, o których należy pamiętać:

  • CString MFC wywołuje bibliotekę czasu wykonywania języka C w celu dynamicznego CString przydzielania pamięci. Ogólnie rzecz biorąc, CString jest tak wydajny, jak każdy inny dynamicznie przydzielony ciąg. Podobnie jak w przypadku każdego dynamicznie przydzielonego ciągu, obciążenie związane z alokacją dynamiczną i wydaniem. Często prosta char tablica na stosie może obsługiwać ten sam cel i jest szybsza. Nie używaj parametru do CString przechowywania ciągu ciągłego. Użycie w zamian parametru const char *. Każda operacja wykonywana za pomocą CString obiektu ma pewne obciążenie. Korzystanie z funkcji ciągów biblioteki czasu wykonywania może być szybsze.

  • CArray Zapewnia CArray elastyczność, że tablica regularna nie jest, ale twój program może tego nie potrzebować. Jeśli znasz określone limity dla tablicy, możesz zamiast tego użyć globalnej stałej tablicy. Jeśli używasz polecenia CArray, użyj polecenia CArray::SetSize , aby ustalić jego rozmiar i określić liczbę elementów, za pomocą których rośnie, gdy konieczne jest reallokowanie. W przeciwnym razie dodanie elementów może spowodować, że tablica będzie często ponownie przydzielana i kopiowana, co jest nieefektywne i może fragmentować pamięć. Ponadto jeśli wstawisz element do tablicy, CArray przenosi kolejne elementy w pamięci i może być konieczne zwiększenie tablicy. Te akcje mogą powodować błędy pamięci podręcznej i błędy stron. Jeśli zapoznasz się z kodem używanym przez MFC, możesz zobaczyć, że możesz napisać coś bardziej szczegółowego dla danego scenariusza w celu zwiększenia wydajności. Ponieważ CArray jest to na przykład szablon, możesz podać CArray specjalizacje dla określonych typów.

  • CListCList jest podwójnie połączoną listą, więc wstawienie elementu jest szybkie na głowie, ogonie i na znanej pozycji (POSITION) na liście. Wyszukiwanie elementu według wartości lub indeksu wymaga jednak wyszukiwania sekwencyjnego, co może być powolne, jeśli lista jest długa. Jeśli kod nie wymaga podwójnie połączonej listy, warto ponownie rozważyć użycie polecenia CList. Użycie listy połączonej singly zapisuje obciążenie związane z aktualizowaniem innego wskaźnika dla wszystkich operacji i pamięci dla tego wskaźnika. Dodatkowa pamięć nie jest duża, ale jest to kolejna okazja do chybienia pamięci podręcznej lub błędów strony.

  • IsKindOf Ta funkcja może generować wiele wywołań i może uzyskiwać dostęp do pamięci w różnych obszarach danych, co prowadzi do nieprawidłowej lokalizacji odwołania. Jest to przydatne w przypadku kompilacji debugowania (na przykład w wywołaniu ASSERT), ale spróbuj unikać używania jej w kompilacji wydania.

  • PreTranslateMessage Użyj PreTranslateMessage , gdy określone drzewo okien wymaga różnych akceleratorów klawiatury lub kiedy należy wstawić obsługę komunikatów do pompy komunikatów. PreTranslateMessage zmienia komunikaty wysyłania MFC. Jeśli zastąpisz PreTranslateMessagepolecenie , zrób to tylko na wymaganym poziomie. Na przykład nie jest konieczne zastąpienie, jeśli interesuje Cię tylko komunikaty przechodzące CMainFrame::PreTranslateMessage do elementów podrzędnych określonego widoku. Zastąpij PreTranslateMessage zamiast tego klasę widoku.

    Nie należy pomijać normalnej ścieżki wysyłania przy użyciu polecenia PreTranslateMessage w celu obsługi komunikatów wysyłanych do dowolnego okna. W tym celu należy użyć procedur okien i map komunikatów MFC.

  • OnIdle Zdarzenia bezczynności mogą wystąpić czasami, gdy nie oczekujesz, na przykład między zdarzeniami WM_KEYDOWN i WM_KEYUP . Czasomierze mogą być bardziej wydajnym sposobem wyzwalania kodu. Nie wymuszaj OnIdle wywoływanej wielokrotnie przez generowanie fałszywych komunikatów lub przez zawsze zwracanie TRUE z przesłonięcia wartości , co nigdy nie pozwoliłoby na uśpienie wątku OnIdle. Ponownie czasomierz lub oddzielny wątek może być bardziej odpowiedni.

Biblioteki udostępnione

Ponowne użycie kodu jest pożądane. Jeśli jednak zamierzasz użyć kodu innego użytkownika, upewnij się, że wiesz dokładnie, co robi w tych przypadkach, gdy wydajność ma krytyczne znaczenie dla Ciebie. Najlepszym sposobem zrozumienia jest przejście przez kod źródłowy lub pomiar za pomocą narzędzi, takich jak PView lub monitor wydajności.

Stert

Używaj wielu stert z dyskrecjami. Dodatkowe sterty utworzone za pomocą HeapCreate polecenia i HeapAlloc umożliwiają zarządzanie, a następnie usuwanie powiązanego zestawu alokacji. Nie zatwierdzaj zbyt dużej ilości pamięci. Jeśli używasz wielu stert, zwróć szczególną uwagę na ilość pamięci, która jest początkowo zatwierdzana.

Zamiast wielu stert, można użyć funkcji pomocnika do interfejsu między kodem a domyślnym stertą. Funkcje pomocnicze ułatwiają niestandardowe strategie alokacji, które mogą poprawić wydajność aplikacji. Jeśli na przykład często wykonujesz małe alokacje, możesz chcieć zlokalizować te alokacje do jednej części domyślnej sterty. Możesz przydzielić duży blok pamięci, a następnie użyć funkcji pomocniczej, aby suballocate z tego bloku. Następnie nie będziesz mieć wielu sterty z nieużywaną pamięcią, ponieważ alokacja wychodzi z domyślnej sterty.

W niektórych przypadkach jednak użycie domyślnej sterty może zmniejszyć lokalność odwołania. Użyj Przeglądarki procesów, Spy++lub monitor wydajności, aby zmierzyć efekty przenoszenia obiektów ze sterta do sterta.

Zmierz sterty, aby można było uwzględnić każdą alokację na stercie. Użyj procedur stert debugowania w czasie wykonywania języka C, aby punkt kontrolny i zrzucić stertę. Dane wyjściowe można odczytać do programu arkusza kalkulacyjnego, takiego jak program Microsoft Excel, i użyć tabel przestawnych, aby wyświetlić wyniki. Zwróć uwagę na łączną liczbę, rozmiar i rozkład alokacji. Porównaj te wyniki z rozmiarem zestawów roboczych. Przyjrzyj się również klastrowaniu powiązanych obiektów o rozmiarze.

Możesz również użyć liczników wydajności do monitorowania użycia pamięci.

Wątki

W przypadku zadań w tle efektywna bezczynność obsługi zdarzeń może być szybsza niż w przypadku korzystania z wątków. Łatwiej jest zrozumieć lokalność odwołań w programie jednowątkowym.

Dobrą regułą jest użycie wątku tylko wtedy, gdy blok systemu operacyjnego znajduje się w katalogu głównym pracy w tle. Wątki są najlepszym rozwiązaniem w takim przypadku, ponieważ niepraktyczne jest blokowanie głównego wątku na zdarzeniu.

Wątki przedstawiają również problemy z komunikacją. Musisz zarządzać łączem komunikacyjnym między wątkami z listą komunikatów lub przydzielaniem i używaniem pamięci udostępnionej. Zarządzanie linkiem komunikacyjnym zwykle wymaga synchronizacji, aby uniknąć warunków wyścigu i problemów z impasem. Ta złożoność może łatwo przekształcić się w błędy i problemy z wydajnością.

Aby uzyskać więcej informacji, zobacz Przetwarzanie pętli bezczynności i wielowątkowość.

Mały zestaw roboczy

Mniejsze zestawy robocze oznaczają lepszą lokalność odwołań, mniej błędów stron i więcej trafień pamięci podręcznej. Zestaw roboczy procesu to najbliższa metryka, która zapewnia system operacyjny bezpośrednio do pomiaru lokalności odwołania.

  • Aby ustawić górne i dolne limity zestawu roboczego, użyj polecenia SetProcessWorkingSetSize.

  • Aby uzyskać górne i dolne limity zestawu roboczego, użyj polecenia GetProcessWorkingSetSize.

  • Aby wyświetlić rozmiar zestawu roboczego, użyj programu Spy++.

Zobacz też

Optymalizacja kodu