Optymalizacje ogólne
Oto niektóre z moich ulubionych optymalizacji. W rzeczywistości zwiększyłem czas wykonywania i zmniejszyłem rozmiary programów, używając ich.
Zadeklaruj małe funkcje jako inline
makra lub
Każde wywołanie funkcji (lub metody) wiąże się z narzutem, takim jak umieszczanie zmiennych na stosie. Niektóre funkcje mogą również wiązać się z kosztami po powrocie. Nieefektywna funkcja lub metoda ma mniej instrukcji w swojej treści niż połączony narzut. Są to dobrzy kandydaci do wstawiania, zarówno jako #define
makra, jak i inline
funkcje. (Tak, wiem, że inline
to tylko sugestia, ale w tym przypadku traktuję to jako przypomnienie dla kompilatora).
Usuń martwy i zbędny kod
Jeśli kod nie jest używany lub nie ma wpływu na wynik programu, pozbądź się go.
Uprość projektowanie algorytmów
Kiedyś usunąłem dużo kodu asemblera i czasu wykonywania z programu, zapisując równanie algebraiczne, które obliczał, a następnie uprościłem wyrażenie algebraiczne. Realizacja uproszczonego wyrażenia algebraicznego zajęła mniej miejsca i czasu niż pierwotna funkcja.
Rozwijanie pętli
Każda pętla ma narzut związany z zwiększaniem i sprawdzaniem zakończenia. Aby uzyskać oszacowanie współczynnika wydajności, policz liczbę instrukcji w narzutu (minimum 3: zwiększ, sprawdź, przejdź do początku pętli) i podziel przez liczbę instrukcji wewnątrz pętli. Im niższa liczba, tym lepiej.
Edycja: podaj przykład rozwijania pętli Przed:
unsigned int sum = 0;
for (size_t i; i < BYTES_TO_CHECKSUM; ++i)
{
sum += *buffer++;
}
Po rozwinięciu:
unsigned int sum = 0;
size_t i = 0;
**const size_t STATEMENTS_PER_LOOP = 8;**
for (i = 0; i < BYTES_TO_CHECKSUM; **i = i / STATEMENTS_PER_LOOP**)
{
sum += *buffer++; // 1
sum += *buffer++; // 2
sum += *buffer++; // 3
sum += *buffer++; // 4
sum += *buffer++; // 5
sum += *buffer++; // 6
sum += *buffer++; // 7
sum += *buffer++; // 8
}
// Handle the remainder:
for (; i < BYTES_TO_CHECKSUM; ++i)
{
sum += *buffer++;
}
Zaletą tej korzyści jest dodatkowa korzyść: wykonywanych jest więcej instrukcji, zanim procesor będzie musiał ponownie załadować pamięć podręczną instrukcji.
Osiągnąłem niesamowite rezultaty, kiedy rozwinąłem pętlę do 32 instrukcji. Było to jednym z wąskich gardeł, ponieważ program musiał obliczyć sumę kontrolną pliku o wielkości 2 GB. Ta optymalizacja w połączeniu z odczytem bloków poprawiła wydajność od 1 godziny do 5 minut. Rozwijanie pętli zapewniało doskonałą wydajność również w języku asemblera, mój memcpy
był dużo szybszy niż kompilator memcpy
. - TM
Redukcja if
instrukcji
Procesory nienawidzą rozgałęzień lub skoków, ponieważ zmusza procesor do ponownego załadowania kolejki instrukcji.
Boolean Arithmetic ( Edytowano: zastosowany format kodu do fragmentu kodu, dodany przykład)
Konwertuj if
instrukcje na przypisania logiczne. Niektóre procesory mogą warunkowo wykonywać instrukcje bez rozgałęziania:
bool status = true;
status = status && /* first test */;
status = status && /* second test */;
Krótki spięciom z logicznego I operatora ( &&
) uniemożliwia wykonywanie testów jeżeli status
jest false
.
Przykład:
struct Reader_Interface
{
virtual bool write(unsigned int value) = 0;
};
struct Rectangle
{
unsigned int origin_x;
unsigned int origin_y;
unsigned int height;
unsigned int width;
bool write(Reader_Interface * p_reader)
{
bool status = false;
if (p_reader)
{
status = p_reader->write(origin_x);
status = status && p_reader->write(origin_y);
status = status && p_reader->write(height);
status = status && p_reader->write(width);
}
return status;
};
Alokacja zmiennej czynnika poza pętlami
Jeśli zmienna jest tworzona w locie wewnątrz pętli, przenieś tworzenie / alokację na miejsce przed pętlą. W większości przypadków zmienna nie musi być przydzielana podczas każdej iteracji.
Czynnikowe wyrażenia stałe poza pętlami
Jeśli wartość obliczenia lub zmiennej nie zależy od indeksu pętli, przenieś ją poza pętlę (przed).
I / O w blokach
Odczyt i zapis danych w dużych porcjach (blokach). Im większy tym lepszy. Na przykład czytanie jednego oktektu na raz jest mniej wydajne niż czytanie 1024 oktetów przy jednym odczycie.
Przykład:
static const char Menu_Text[] = "\n"
"1) Print\n"
"2) Insert new customer\n"
"3) Destroy\n"
"4) Launch Nasal Demons\n"
"Enter selection: ";
static const size_t Menu_Text_Length = sizeof(Menu_Text) - sizeof('\0');
//...
std::cout.write(Menu_Text, Menu_Text_Length);
Skuteczność tej techniki można wykazać wizualnie. :-)
Nie używaj printf
rodziny do stałych danych
Stałe dane można wyprowadzać za pomocą zapisu blokowego. Sformatowany zapis marnuje czas na skanowanie tekstu pod kątem formatowania znaków lub przetwarzania poleceń formatujących. Zobacz powyższy przykład kodu.
Sformatuj do pamięci, a następnie napisz
Sformatuj do char
tablicy przy użyciu wielu sprintf
, a następnie użyj fwrite
. Umożliwia to również podział układu danych na „sekcje stałe” i sekcje zmienne. Pomyśl o korespondencji seryjnej .
Zadeklaruj stały tekst (literały ciągów) jako static const
Gdy zmienne są deklarowane bez static
, niektóre kompilatory mogą przydzielić miejsce na stosie i skopiować dane z pamięci ROM. To są dwie niepotrzebne operacje. Można to naprawić za pomocą static
prefiksu.
Wreszcie, kod taki jak kompilator
Czasami kompilator może lepiej zoptymalizować kilka małych instrukcji niż jedną skomplikowaną wersję. Pomocne jest również pisanie kodu, który pomoże kompilatorowi w optymalizacji. Jeśli chcę, aby kompilator korzystał ze specjalnych instrukcji transferu bloków, napiszę kod, który wygląda tak, że powinien korzystać ze specjalnych instrukcji.