Diversity w polskim IT
Chaulio Ferreira
Chaulio FerreiraAndroid Developer @ 3D Reality Maps GmbH

Jak mierzyć czas wykonania programu w C/C++

Poznaj kilka sposobów na zmierzenie czasu wykonania programu w C lub C++, w zależności od systemu operacyjnego i tego, co właściwie mierzymy.
21.01.202110 min
Jak mierzyć czas wykonania programu w C/C++

Pomiar czasu wykonania programu C/C++ (w całości lub w częściach) jest czasem trudniejszy, niż może się wydawać. A dzieje się tak, ponieważ wiele metod nie przenosi się na inne platformy. Wybór odpowiedniej metody będzie w dużej mierze zależał od Twojego systemu operacyjnego, wersji kompilatora, oraz rozumienia „czasu”. Artykuł ten zawiera listę najlepszych obecnie dostępnych opcji. Analizując je, przyjrzymy się również ich ograniczeniom. Mam więc nadzieję, że znajdziesz w tym artykule coś dla siebie. 

Czas rzeczywisty kontra czas pracy procesora

Najpierw zdefiniujemy i dokonamy charakterystyki tych dwóch terminów. Używamy ich do pomiaru czasu wykonania programu. 

  1. Czas rzeczywisty (ang. wall time) - to całkowity czas, który upłynął podczas pomiaru. Możesz go mierzyć nawet przy pomocy stopera, o ile potrafisz zacząć i skończyć dokładnie w momencie startu i zakończenia pracy programu. 
  2. Czas pracy procesora (ang. CPU time) - odnosi się do czasu, w którym procesor przetwarzał instrukcje programu. Nie należy do niego jednak czas spędzony na oczekiwaniu na zakończenie różnych akcji, w tym operacji I/O. 


Wybór jednego z powyższych zależy od powodu, dla którego w ogóle mierzysz czas wykonywania swojego programu. Jeśli chodzi o poniższą listę, to większość metod dokonuje obliczeń tylko według jednej definicji. Kilka z nich jest jednak w stanie obliczyć czas wykonania według dwóch. 

Co ważne, niektóre z nich są dostępne zarówno dla Linuksa, jak i dla Windowsa. Pozostałe działają tylko w jednym z tych systemów operacyjnych. Na początku każdej sekcji zobaczycie definicję czasu, według której mierzymy, oraz system, na którym nasza metoda działa. 

Na temat kodu...

Nasze przykłady opierają się na programach, które obliczają przybliżenie dla nieskończonej sumy: 1/2⁰ + 1/2¹ + 1/2² + 1/2³ + … = 2. 

100 iteracji po pętli to już sporo, jeśli chcemy otrzymać dokładną sumę (tak wyszło na mojej maszynie - Twoje wyniki mogą się różnić), a przedstawione programy wykonują nawet miliard iteracji, aby zdobyć odpowiednią ilość czasu do pomiaru. 

W naszych programach procesor jest zajęty przez cały czas, więc nie będzie różnicy między czasem rzeczywistym, a czasem procesora. Jeśli chcesz, aby Twój procesor był przez jakiś czas bezczynny, to możesz to uzyskać dzięki następującej funkcji: sleep() (dostępny w <unistd.h>).

Zaczynajmy!

1. time - komenda Linuxa

Działa tylko na Linuxie (może zostać użyta dla każdego programu, który da się wykonać z poziomu terminala). 

Mierzy zarówno czas rzeczywisty, jak i czas procesora.

Nie do końca jest to kod C/C++. Będzie to jednak prawdopodobnie wystarczające dla tych, którzy chcą odpalać swoje programy na Linuksie - to dlatego postanowiłem umieścić tę opcję przed tymi bardziej skomplikowanymi. Jeśli chcesz zmierzyć czas rzeczywisty i CPU dla całego programu, to nie musisz nic zmieniać w kodzie.  

Po prostu wpisz time przed komendą, którą zwykle uruchamiasz program. Potem Twój pomiar pokaże się na ekranie:

$ time ./MyProgram
Result: 2.00000000000000000000

real 0m5.931s
user 0m5.926s
sys 0m0.005s


Gdy spojrzymy na wynik, "real" oznacza czas rzeczywisty, a "user" - CPU time. Mamy tutaj zatem dwa pomiary na cały program, a nie zmieniliśmy ani jednej linijki kodu. Jeśli chcielibyśmy jednak mierzyć czas wykonania poszczególnych części programu, to potrzebowalibyśmy jednej z poniższych opcji. 

Uwaga: zanim stworzyłem ten artykuł, myślałem, że Windows ma swoją własną wersję komendy time. Byłem więc mocno zaskoczony, gdy okazało się inaczej. 

Oto kilka alternatyw, ale dodanie pomiaru czasu bezpośrednio w kodzie C/C++ powinno się lepiej przenosić. 

2. Korzystanie z <chrono>

Działa na Linuxie i Windowsie, ale wymaga C++ 11 lub nowszego.

Mierzy czas rzeczywisty.

Jest to teraz najprawdopodobniej najlepszy i najłatwiejszy do przeniesienia sposób na pomiar czasu rzeczywistego. Jest on jednak dostępny tylko w C++ 11 lub wersjach nowszych. Jeśli Twój projekt lub kompilator nie obsługuje C++ 11, to musisz spróbować którejś z innych opcji z listy. 

Biblioteka <chrono> ma dostęp do kilku różnych zegarów w Twojej maszynie, a każdy z nich ma inne przeznaczenie. Każdy rodzaj zegara jest szczegółowo opisany tutaj. Polecam jednak użycie high_resolution_clock (chyba że masz inne potrzeby).

Wskazany przeze mnie zegar używa jednak możliwie największej rozdzielczości czasu, a więc będzie odpowiedni dla większości ludzi. Oto jak go używać:  

#include <stdio.h>
#include <chrono>

int main () {
    double sum = 0;
    double add = 1;

    // Start measuring time
    auto begin = std::chrono::high_resolution_clock::now();
    
    int iterations = 1000*1000*1000;
    for (int i=0; i<iterations; i++) {
        sum += add;
        add /= 2.0;
    }
    
    // Stop measuring time and calculate the elapsed time
    auto end = std::chrono::high_resolution_clock::now();
    auto elapsed = std::chrono::duration_cast<std::chrono::nanoseconds>(end - begin);
    
    printf("Result: %.20f\n", sum);
    
    printf("Time measured: %.3f seconds.\n", elapsed.count() * 1e-9);
    
    return 0;
}


Jak widać w linijce 19, przedstawiliśmy czas w nanosekundach (chociaż później przekonwertujemy go na sekundy). Jak wolisz, to możesz zmodyfikować kod tak, aby korzystał z jakiejś innej jednostki przy pomocy chrono::hours, chrono::minutes, chrono::seconds, chrono::milliseconds, lub chrono::microseconds.

Uwaga: słyszałem głosy, jakoby wykorzystywanie tej biblioteki do pomiaru czasu wykonania dodawało dużo narzutu, w przeciwieństwie do innych metod C/C++, zwłaszcza kiedy używamy jej kilka razy w jednej pętli. 

Szczerze mówiąc, nie przytrafiło mi się raczej nic takiego, więc tego nie skomentuję. Jeśli jednak coś takiego przytrafi się Tobie, to rozważyłbym wykorzystanie jednej z pozostałych opcji. 

3. Korzystanie z <sys/time.h> oraz gettimeofday()

Działa zarówno na Linuxie, jak i na Windowsie.

Mierzy czas rzeczywisty. 

Funkcja gettimeofday() zwraca czas, jaki upłynął od 00:00:00 UTC 1 stycznia 1970 roku (często jest to określane jako czas Unixowy). Trudność może tutaj sprawić fakt, że funkcja zwraca zarówno liczbę sekund, jak i mikrosekund jako oddzielne zmienne int. Aby uzyskać całkowity czas, włączając w to mikrosekundy, trzeba obie zmienne zsumować. Oto jak to zrobić:

#include <stdio.h>
#include <sys/time.h>

int main () {
    double sum = 0;
    double add = 1;

    // Start measuring time
    struct timeval begin, end;
    gettimeofday(&begin, 0);
    
    int iterations = 1000*1000*1000;
    for (int i=0; i<iterations; i++) {
        sum += add;
        add /= 2.0;
    }
    
    // Stop measuring time and calculate the elapsed time
    gettimeofday(&end, 0);
    long seconds = end.tv_sec - begin.tv_sec;
    long microseconds = end.tv_usec - begin.tv_usec;
    double elapsed = seconds + microseconds*1e-6;
    
    printf("Result: %.20f\n", sum);
    
    printf("Time measured: %.3f seconds.\n", elapsed);
    
    return 0;
}


Pierwsza uwaga: jeśli nie obchodzą Cię ułamki sekund, upływający czas możesz sprawdzić dzięki obliczeniu end.tv_sec - begin.tv_sec. 

Druga uwaga: drugi argument gettimeofday() używany jest do określenia właściwej strefy czasowej. Ale ponieważ obliczamy upływ czasu, to strefy czasowe są tutaj nieistotne, o ile taka sama wartość zostanie użyta zarówno dla początku i końca. To dlatego użyliśmy 0 dla obu wywołań. 

4. Korzystanie z <time.h> i time()

Działa zarówno na Linuksie, jak i na Windowsie.

Mierzy czas rzeczywisty, ale tylko w pełnych sekundach.

Funkcja time() jest podobna do gettimeofday(), ponieważ zwraca czas, jaki upłynął od 1 stycznia 1970. Są tutaj jednak dwie spore różnice: po pierwsze, nie jesteśmy w stanie określić strefy czasowej, a więc zawsze mamy UTC. Po drugie, i najważniejsze, zwraca tylko pełne sekundy. 

To dlatego pomiar czasu wykonania przy pomocy tej metody ma sens tylko wtedy, jeśli interwały są dłuższe niż kilka sekund. Jeśli mamy mili, mikro, czy nanosekundy, to należy użyć innej metody.

A oto jak korzystać z tej:

#include <stdio.h>
#include <time.h>

int main () {
    double sum = 0;
    double add = 1;

    // Start measuring time
    time_t begin, end;
    time(&begin);
    
    int iterations = 1000*1000*1000;
    for (int i=0; i<iterations; i++) {
        sum += add;
        add /= 2.0;
    }
    
    // Stop measuring time and calculate the elapsed time
    time(&end);
    time_t elapsed = end - begin;
    
    printf("Result: %.20f\n", sum);
    
    printf("Time measured: %ld seconds.\n", elapsed);
    
    return 0;
}


Uwaga:
time_t to właściwie to samo, co long int, a więc wynik można bezpośrednio wydrukować w printf(), czy cout. Można też wrzucić to do jakiegokolwiek typu numerycznego. 

5. Korzystanie z <time.h> i clock()

Działa zarówno na Linuksie, jak i na Windowsie. 

Mierzy czas pracy procesora na Linuksie i czas rzeczywisty na Windowsie.

Funkcja clock() zwraca liczbę tyknięć zegara od momentu, w którym program zaczął się uruchamiać. Jeśli podzieli się to przez stałą CLOCKS_PER_SEC, to zobaczymy w sekundach, ile czasu program już działa. 

Nasz wynik będzie miał jednak inne znaczenie w zależności od systemu operacyjnego: na Linuxie otrzymamy wynik czasu pracy procesora, a Windows pokaże nam wall time. Trzeba więc tutaj uważać.

A oto kod:

#include <stdio.h>
#include <time.h>

int main () {
    double sum = 0;
    double add = 1;

    // Start measuring time
    clock_t start = clock();
    
    int iterations = 1000*1000*1000;
    for (int i=0; i<iterations; i++) {
        sum += add;
        add /= 2.0;
    }

    // Stop measuring time and calculate the elapsed time
    clock_t end = clock();
    double elapsed = double(end - start)/CLOCKS_PER_SEC;
    
    printf("Result: %.20f\n", sum);
    
    printf("Time measured: %.3f seconds.\n", elapsed);
    
    return 0;
}


Uwaga:
clock_t to również long int, a więc trzeba wrzucić go do typu liczby zmiennoprzecinkowej, zanim będziemy dzielić przez CLOCKS_PER_SEC. W innym przypadku będziemy mieli dzielenie na liczbach całkowitych. 

6. <time.h> i clock_gettime()

Działanie: tylko Linux. 

Mierzy czas rzeczywisty i czas pracy procesora. 

Fajną rzeczą jest tutaj to, że możemy użyć tej metody do pomiaru zarówno czasu rzeczywistego, jak i czasu pracy procesora. Jest ona jednak dostępna jedynie w systemach Unixowych. 

Poniższy przykład pokazuje pomiar czasu rzeczywistego, ale można go zmodyfikować, aby mierzył czas pracy procesora przez zastąpienie stałej CLOCK_REALTIME stałą CLOCK_PROCESS_CPUTIME_ID.

#include <stdio.h>
#include <time.h>

int main () {
    double sum = 0;
    double add = 1;

    // Start measuring time
    struct timespec begin, end; 
    clock_gettime(CLOCK_REALTIME, &begin);
    
    int iterations = 1000*1000*1000;
    for (int i=0; i<iterations; i++) {
        sum += add;
        add /= 2.0;
    }
    
    // Stop measuring time and calculate the elapsed time
    clock_gettime(CLOCK_REALTIME, &end);
    long seconds = end.tv_sec - begin.tv_sec;
    long nanoseconds = end.tv_nsec - begin.tv_nsec;
    double elapsed = seconds + nanoseconds*1e-9;
    
    printf("Result: %.20f\n", sum);
    
    printf("Time measured: %.3f seconds.\n", elapsed);
    
    return 0;
}


Uwaga 1:
Oprócz CLOCK_REALTIME i CLOCK_PROCESS_CPUTIME_ID istnieją inne zegary, których możesz użyć razem z tą funkcją. Obszerniejsza lista znajduje się tutaj

Uwaga 2: struktura timespec, z której korzysta ta funkcja, jest podobna do tej, której używa gettimeofday(). Mamy tam jednak nanosekundy zamiast mikrosekund, a więc należy uważać przy konwersji jednostek. 

7. <sysinfoapi.h> i GetTickCount64()

Działa tylko na Windowsie.

Mierzy czas rzeczywisty. 

Funkcja GetTickCount64() zwraca liczbę milisekund od kiedy system wystartował. Mamy tutaj wersję 32-bitową (GetTickCount()), ale jej limit to 49.71 dnia, a więc bezpieczniej będzie użyć wersji 64-bitowej. 

Oto jak to zrobić:

#include <stdio.h>
#include <sysinfoapi.h>

int main () {
    double sum = 0;
    double add = 1;

    // Start measuring time
    long long int begin = GetTickCount64();

    int iterations = 1000*1000*1000;
    for (int i=0; i<iterations; i++) {
        sum += add;
        add /= 2.0;
    }

    // Stop measuring time and calculate the elapsed time
    long long int end = GetTickCount64();
    double elapsed = (end - begin)*1e-3;

    printf("Result: %.20f\n", sum);

    printf("Time measured: %.3f seconds.\n", elapsed);

    return 0;
}

8. <processthreadsapi.h> and GetProcessTimes()

Działa tylko na Windowsie.

Mierzy czas pracy procesora.

Jest to najbardziej skomplikowana metoda na liście, ale jedyna, dzięki której zmierzymy czas pracy procesora na Windowsie. Nie będę tutaj wchodził w szczegóły, bo trochę tego za dużo, i sam nigdy nie używałem tej metody. Podeślę Wam jednak oficjalną dokumentację, którą znajdziecie tutaj. A oto kod: 

#include <stdio.h>
#include <processthreadsapi.h>

double get_cpu_time(){
    FILETIME a,b,c,d;
    if (GetProcessTimes(GetCurrentProcess(),&a,&b,&c,&d) != 0){
        //  Returns total user time.
        //  Can be tweaked to include kernel times as well.
        return
            (double)(d.dwLowDateTime |
            ((unsigned long long)d.dwHighDateTime << 32)) * 0.0000001;
    }else{
        //  Handle error
        return 0;
    }
}

int main () {
    double sum = 0;
    double add = 1;

    // Start measuring time
    double begin = get_cpu_time();

    int iterations = 1000*1000*1000;
    for (int i=0; i<iterations; i++) {
        sum += add;
        add /= 2.0;
    }

    // Stop measuring time and calculate the elapsed time
    double end = get_cpu_time();
    double elapsed = (end - begin);

    printf("Result: %.20f\n", sum);

    printf("Time measured: %.3f seconds.\n", elapsed);

    return 0;
}


Uwaga:
powyższy kod pochodzi ze Stack Overflow. Chciałbym więc podziękować użytkownikowi Alexander Yee, który zapostował odpowiedź. Pokazuje on fajny i możliwy do przeniesienia sposób na obliczenie czasu rzeczywistego i czasu pracy procesora na Linuxie oraz Windowsie przy użyciu makra #ifdef. Sprawdźcie całą odpowiedź. 

Podsumowanie

Pokazałem tutaj kilka sposobów na mierzenie czasu wykonania w C/C++. Jak widać, nie ma jednego uniwersalnego rozwiązania: wszystkie powyższe metody mają ograniczenia i żadna z nich nie jest w stanie obliczyć zarówno czasu rzeczywistego, jak i czasu pracy procesora i być jednocześnie dostępna dla Linuxa i dla Windowsa. 

Ale przynajmniej niektóre z tych metod powinny zadziałać. 

Źródła

Jeśli potrzebujesz dodatkowych informacji, to poniżej znajdziesz bardziej szczegółową dokumentację dla każdej metody wymienionej w tym artykule:

  1. Komenda ‘time’ Linuxa
  2. <chrono>
  3. gettimeofday()
  4. time()
  5. clock()
  6. clock_gettime()
  7. GetTickCount64()
  8. GetProcessTimes()


Oryginał tekstu w języku angielskim możesz przeczytać tutaj.

<p>Loading...</p>