PP – Pomiary czasu

Czas rzeczywisty i czas procesora

Mierzenie czasu wykonania programu lub jego części jest sporym wyzwaniem, o czym mogliśmy przekonać się we wpisie Laboratorium 5. Odpowiednie dobranie sposobu wykonywania pomiaru czasu jest istotną kwestią, ponieważ wiele metod często nie jest przenoszonych na inne platformy. Wybór właściwej metody będzie w dużej mierze zależał od systemu operacyjnego, wersji kompilatora, a przede wszystkim od tego, co rozumiemy pod pojęciem „czas”. W pomiarach czasu możemy wyróżnić czas zegarowy (ang. wall time) oraz czas procesora (ang. CPU time). Czas zegarowy definiujemy jako całkowity czas, jaki upłynął podczas pomiaru. Jest to czas, który można odmierzyć stoperem, zakładając, że jesteśmy w stanie uruchomić i zatrzymać go dokładnie w odpowiednich, zakładanych punktach wykonania programu. Zaś czas procesora odnosi się do czasu, w którym procesor był zajęty przetwarzaniem instrukcji programu. Precyzując, czas oczekiwania np. na operacje wejścia i wyjścia nie jest wliczany do czasu procesora. Znając już powyższe definicji dużo łatwiej jest dobrać metodę, którą powinniśmy mierzyć czas wykonywania naszego programu.

W tym wpisie skupimy się na przedstawieniu prawdopodobnie najlepszej i najłatwiejszej metody do pomiaru czasu rzeczywistego. Jednakże zanim się tym zajmiemy wróćmy do pomiarów czasu wykonywanych za pomocą metod z pliku nagłówkowego ctime:

Plik nagłówkowy <ctime>
#include <iostream>
#include <ctime>

unsigned int fib_rec(unsigned int n) {
    return n<2 ? n : (fib_rec(n-2) + fib_rec(n-1));
}

int main() {
    unsigned int N = 45;
    time_t begin, end;
    
    time(&begin);
    std::cout << fib_rec(N) << std::endl;
    time(&end);
    
    time_t elapsed = end - begin;
    std::cout << elapsed << std::endl;
    return 0;
}

W celu usystematyzowania pomiarów wszystkie poniższe przykłady będą wykonywały obliczenie wartości N-tego elementu ciągu Fibonacciego.

Pomiar czasu wykonania przy pomocy wyżej przedstawionej 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 lub w przypadku tak krótkotrwałych zadań można rozważyć pomiar czasu dla ich wielokrotnego wykonania (np. w pętli). Warto dodać, że time_t to właściwie to samo, co long int.

Inny sposób, który poznaliśmy umożliwiał nam obliczenie czasu pracy procesora, nie tylko czasu rzeczywistego:

#include <iostream>
#include <ctime>

unsigned int fib_rec(unsigned int n) {
    return n<2 ? n : (fib_rec(n-2) + fib_rec(n-1));
}

int main() {
    unsigned int N = 45;

    clock_t start = clock();
    std::cout << fib_rec(N) << std::endl;
    clock_t end = clock();
    
    double elapsed = double(end - start)/CLOCKS_PER_SEC;
    std::cout << elapsed << std::endl;
    return 0;
}

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 zajeło wykonanie wybranego fragmentu kodu. Jednakże istotne jest to, że nasz wynik będzie miał inne znaczenie w zależności od systemu operacyjnego: na Linuxie otrzymamy wynik czasu pracy procesora, a Windows pokaże nam czas zegarowy (clock_t to również long int, a więc trzeba dokonać jawnej konwersji do typu liczby zmiennoprzecinkowej, zanim będziemy dzielić przez CLOCKS_PER_SEC). Istotnym jest fakt, że w przypadku współbieżnego (wielowątkowego) wykonywania sekcji, dla której mierzymy czas, zwrócony zostanie czas sumaryczny wszystkich wątków, który tym samym może być znacznie dłuższy niż rzeczywisty pomiar.

Plik nagłówkowy <chrono>

Przejdziemy teraz do kolejnej formy pomiaru czasu, która prawdopodobnie okaże sie najdokładniejszą metodą do mierzenia czasu rzeczywistego. Metoda ta jest dostępna od wersji C++ 11. W tym celu wykorzystamy bibliotekę chrono. 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 w dokumentacji. W poniższym przykładzie wykorzystamy high_resolution_clock, którego zaleca się używać w większości przypadków. Wskazany zegar używa możliwie największej rozdzielczości czasu.

#include <iostream>
#include <chrono>

unsigned int fib_rec(unsigned int n) {
    return n<2 ? n : (fib_rec(n-2) + fib_rec(n-1));
}

int main() {
    unsigned int N = 45;

    auto begin = std::chrono::high_resolution_clock::now();
    std::cout << fib_rec(N) << std::endl;
    auto end = std::chrono::high_resolution_clock::now();
    
    auto elapsed = std::chrono::duration_cast<std::chrono::nanoseconds>(end - begin);
    std::cout << elapsed.count() * 1e-9 << std::endl; //nano -> sec  (* 1e-9)
    return 0;
}

Wykorzystywanie biblioteki chrono umożliwia przedstawienie czasu w wielu jednostkach np. nanosekund (powyższy kod chrono::nanoseconds) lub przy pomocy chrono::hours, chrono::minutes, chrono::seconds, chrono::milliseconds, lub chrono::microseconds.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *