CodeGym /Kursy /C++ SELF /steady_clock vs system_clock

steady_clock vs system_clock

C++ SELF
Poziom 66, Lekcja 2
Dostępny

1. Wprowadzenie

Jeżeli zdarzyło Ci się pomyśleć „przecież czas jest jeden, po co tyle bytów”, to gratulacje: myślisz jak normalny człowiek, a nie jak twórca biblioteki standardowej. Jednak komputer ma „czas” w dwóch różnych znaczeniach i łatwo je pomylić w kodzie. Właśnie dlatego w std::chrono są różne zegary (clock): jedne służą do rozumienia daty/czasu w sensie ludzkim, inne — do uczciwego mierzenia ile trwała operacja.

Wyobraź sobie dwie sytuacje.

W pierwszej dodajesz zadanie do swojego konsolowego menedżera zadań i chcesz zapisać: „utworzone 16 stycznia 2026, 10:15:03”. To czas kalendarzowy. Ważne, żeby zgadzał się z zegarem systemu, ze strefą czasową użytkownika, z tym co widzi w interfejsie.

W drugiej chcesz zmierzyć: „ile milisekund zajęło posortowanie listy zadań” albo „ile czasu zajęło odczytanie pliku”. To pomiar długości trwania. I tutaj kalendarz zwykle w ogóle nie jest potrzebny. Co więcej, może przeszkadzać, bo czas kalendarzowy czasem skacze.

2. system_clock: czas kalendarzowy, który może skakać

Kiedy programista mówi „aktualny czas”, najczęściej ma na myśli właśnie std::chrono::system_clock. To zegar przeznaczony do czasu kalendarzowego: odpowiada temu, co pokazuje system (i temu, czego człowiek oczekuje). Ten zegar ma swoją epokę i można go przekształcić do std::time_t, co ułatwia formatowanie na „2026-01-16 10:15:03” (szczegółowe formatowanie — temat sąsiedniego wykładu).

Jednocześnie system_clock ma ważną cechę: czas może być zmieniony z zewnątrz. Na przykład użytkownik ręcznie ustawił zegar, system zsynchronizował się przez NTP, nastąpił przejście na/ze strefy letniej (w niektórych konfiguracjach), administrator serwera „podkręcił” czas. Dla kalendarza to w porządku: kalendarz powinien odzwierciedlać rzeczywistość (nawet jeśli jest ona dziwna). Dla pomiarów długości trwania to problem: możesz dostać ujemny czas trwania albo „nagle ogromny”.

Mała idea w kodzie jest taka: system_clock jest dobry do „kiedy”, ale podejrzany do „ile trwało”.

Krótki fragment dla naszej edukacyjnej aplikacji TaskBook (umowny menedżer zadań), gdzie zapisujemy moment utworzenia zadania jako „milisekundy od epoki” — to właśnie poprawna rola system_clock:

#include <chrono>

long long now_ms_since_epoch() {
    const auto now = std::chrono::system_clock::now();
    const auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
        now.time_since_epoch()
    );
    return ms.count();
}

Tu ważne, że wyraźnie rzutujemy time_since_epoch() na milisekundy, bo count() sam w sobie nie musi być „w milisekundach”. (Ta ostrożność to bezpośrednie rozwinięcie tematu duration_cast z poprzedniego wykładu.)

3. steady_clock: zegar do pomiarów, który nie „cofa się”

Teraz przechodzimy do bohatera wykładu — std::chrono::steady_clock. To zegar do pomiaru długości trwania. Kluczowa idea steady_clock jest taka, że jest on monotoniczny: „teraz” na tym zegarze nie powinno być mniejsze niż wcześniej. Mówiąc prościej, czas na steady_clock nie musi zgadzać się z kalendarzem, ale powinien być dobrą linijką.

Gdy mierzysz szybkość wykonania kodu, zależy Ci, żeby „start” był przed „fini szem” (logicznym, prawda). Zegary kalendarzowe mogą niespodziewanie cofnąć się i wtedy finish - start może być ujemne. steady_clock jest zaprojektowany właśnie po to, by do tego nie dopuścić.

Typowy wzorzec pomiaru wygląda nudno, ale działa od lat i zwykle przeżywa nawet zmianę zespołu:

#include <chrono>

std::chrono::milliseconds measure_demo() {
    const auto start = std::chrono::steady_clock::now();
    // ... wykonujemy pracę ...
    const auto finish = std::chrono::steady_clock::now();
    return std::chrono::duration_cast<std::chrono::milliseconds>(finish - start);
}

Zwróć uwagę na semantyczne piękno: odejmujemy dwa momenty (time_point) i otrzymujemy czas trwania (duration). To „poprawna arytmetyka czasu” i czyta się prawie jak wzór.

4. Najważniejsze porównanie: „kiedy się wydarzyło” vs „ile to trwało”

W tym miejscu warto na chwilę zwolnić, bo w głowie początkującego te dwa światy bardzo łatwo się mieszają. Jeśli je pomieszasz — kompilator czasem Cię uratuje (nie zawsze), a czasem dostaniesz błąd, który ujawni się raz na rok i zawsze we wtorek.

Poniżej tabela, która utrwala rolę zegarów. Powinna być traktowana jako „ściąga na monitor”, a nie teoria dla teorii.

Pytanie, które zadajesz Właściwy typ zegara Dlaczego
„Która jest teraz godzina (dla człowieka)?”
std::chrono::system_clock
To czas kalendarzowy, związany z ustawieniami systemu
„Kiedy zadanie zostało utworzone?”
std::chrono::system_clock
Zdarzenie powinno mieć datę/czas zgodne z rzeczywistością
„Ile czasu zajęła operacja?”
std::chrono::steady_clock
Potrzebna jest monotoniczna linijka bez skoków
„Czy to można zapisać do pliku i odczytać jutro?” system_clock (w postaci time_t lub ms since epoch) steady_clock nie ma sensu w „absolutnych datach”

I oto główna wskazówka (bez fanatyzmu, ale zwykle): timestampy — przez system_clock, pomiary — przez steady_clock.

Dlaczego nie można porównywać system_clock i steady_clock bezpośrednio

Bardzo częsty błąd: „weźmiemy start z system_clock, finish z steady_clock, co z tego, oba to time_point”. I tutaj std::chrono pokazuje swoją siłę: różne Clock — różne układy odniesienia i różne typy.

Nawet jeśli dwa wartości wyglądają jak „punkty czasu”, nie są zamienne. Mogą mieć inną epokę, inną naturę pomiaru, inne gwarancje. Mieszanie ich w wyrażeniach to niemal zawsze błąd logiczny, dlatego kompilator zwykle Ci na to nie pozwoli.

Przykład tego, czego nie robić (i dobrze, że się nie kompiluje):

#include <chrono>

int main() {
    const auto a = std::chrono::system_clock::now();
    const auto b = std::chrono::steady_clock::now();

    // auto diff = b - a; // nie wolno: różne zegary
}

Ta „niekompatybilność” nie jest ograniczeniem dla zabawy. To ochrona przed błędem, który potem trudno wytłumaczyć osobie z dala od C++: „dlaczego czas stał się ujemny?”.

Dlaczego system_clock jest zły do pomiarów

Bardzo chciałoby się powiedzieć: „mogę mierzyć tak: auto start = system_clock::now(); ... auto elapsed = now - start; — i pokaże liczbę”. I rzeczywiście, na większości maszyn w większości przypadków pokaże „coś podobnego do prawdy”. Problem w tym, że będzie to prawda dopóki system nie uzna, że trzeba poprawić czas.

Wyobraź sobie serwer synchronizujący czas przez sieć. Może on zrobić korektę wstecz i wtedy zmierzony czas trwania nagle stanie się ujemny. Albo poprawi do przodu i otrzymasz „operacja trwała 5 minut”, chociaż zajęła 20 ms. I najgorsze: może to wydarzyć się u użytkownika i trudno to będzie odtworzyć.

Krótki przykład, który wygląda niewinnie, ale metodycznie jest błędny:

#include <chrono>

std::chrono::milliseconds bad_measurement() {
    const auto start = std::chrono::system_clock::now();
    // ... praca ...
    const auto finish = std::chrono::system_clock::now();
    return std::chrono::duration_cast<std::chrono::milliseconds>(finish - start);
}

Może to „działać”, ale jego kontrakt jest słaby: mierzysz czas zegarem przeznaczonym dla kalendarza, który może być korygowany. Dlatego w kodzie, który ma być niezawodny, lepiej od razu przyjąć: pomiar = steady_clock.

5. Osadzamy czas w aplikacji TaskBook

Teraz dodamy dwie małe, ale praktyczne rzeczy do naszego edukacyjnego projektu konsolowego. Niech to będzie TaskBook: lista zadań, którą już potrafimy przechowywać (np. w JSON z poprzedniego dnia) i wyświetlać w konsoli. Dzisiaj interesuje nas nie format przechowywania per se, a co dokładnie przechowujemy o czasie i jak mierzymy szybkość.

Zacznijmy od modelu zadania: dodamy pole created_at_ms. Z założenia to absolutny czas utworzenia, czyli bierzemy system_clock i zapisujemy „milisekundy od epoki”.

#include <string>

struct Task {
    int id = 0;
    std::string title;
    long long created_at_ms = 0; // ms since epoch (system_clock)
};

Teraz napiszemy funkcję tworzącą zadanie, która ustawia to pole. Zauważ: nie przechowujemy time_point bezpośrednio w strukturze, tylko wkładamy zwykłą liczbę. To prostsze do serializacji i do drukowania, i odpowiada idei „na granicy programu — liczba, wewnątrz logiki — chrono-typy”.

#include <chrono>
#include <string>

Task make_task(int id, const std::string& title) {
    Task t;
    t.id = id;
    t.title = title;
    t.created_at_ms = now_ms_since_epoch();
    return t;
}

Teraz dodajmy „miernik” dla jednej operacji. Na przykład chcemy się dowiedzieć, ile czasu zajmuje dodanie zadania (tak, operacja jest tania, ale to przykład; prawdziwe powody będą później — np. odczyt/zapis, sortowania, filtrowania).

#include <chrono>
#include <vector>

std::chrono::milliseconds add_task_timed(std::vector<Task>& tasks, Task t) {
    const auto start = std::chrono::steady_clock::now();
    tasks.push_back(t);
    const auto finish = std::chrono::steady_clock::now();
    return std::chrono::duration_cast<std::chrono::milliseconds>(finish - start);
}

A teraz — ostrożne wyświetlenie, aby zobaczyć, co się dzieje:

#include <iostream>

int main() {
    std::vector<Task> tasks;

    auto t = make_task(1, "Przeczytać o chrono");
    auto elapsed = add_task_timed(tasks, t);

    std::cout << "Added task in " << elapsed.count() << " ms\n"; // Added task in 0 ms
}

Tu w porządku, że zobaczysz 0 ms: operacja jest zbyt szybka, a milisekundy są zbyt grube. Ważniejsze jest to, że użyłeś właściwych zegarów do właściwego zadania. Jeśli chcesz większej precyzji — możesz drukować mikrosekundy, ale to kwestia wybranej jednostki, nie wyboru zegara.

6. Model mentalny: dwie skale czasu

Czasem najłatwiej zrozumieć różnicę między steady_clock a system_clock, wyobrażając sobie dwie linijki.

Pierwsza linijka — to kalendarz na ścianie. Dzisiaj 16 stycznia 2026. Jutro będzie 17 stycznia. Ale jeśli ktoś podejmie i przestawi kalendarz (albo zmieni datę na telefonie), to „dzisiaj” dla Ciebie zmieni się. To niewygodne, ale w sensie kalendarzowym dopuszczalne: kalendarz odzwierciedla umowę.

Druga linijka — to stoper. Nie musi mówić, jaki jest dziś dzień. Ma uczciwie odmierzać w przód, żebyś mógł powiedzieć: „przebiegłem 100 metrów w 14 sekund”. Jeśli stoper zacznie nagle cofać się, przestanie być stoperem i zmieni się w filozoficzny przyrząd do mierzenia losu.

Tej samej myśli można użyć w schemacie:

flowchart TD
    A[Wydarzenie w świecie: 'zadanie utworzone'] --> B["system_clock::now()"]
    B --> C[Przechowujemy jako ms since epoch]

    D[Operacja w kodzie: 'sortowanie zadań'] --> E["steady_clock::now() start"]
    E --> F[...praca...]
    F --> G["steady_clock::now() finish"]
    G --> H[elapsed = finish - start]

Jeśli masz tę rozgałęzienie w głowie, prawie automatycznie wybierasz właściwy zegar do zadania.

7. Typowe błędy przy porównywaniu steady_clock i system_clock

Błąd nr 1: mierzyć długość trwania przez system_clock, bo „taniej”.
Zwykle kończy się to tym, że kod rzeczywiście wygląda prościej… do pierwszego dziwnego przypadku na prawdziwej maszynie. system_clock może być korygowany przez system, więc pomiary przez niego są potencjalnie niestabilne. Do długości trwania używaj steady_clock, a system_clock zostaw do timestampów i znaczników kalendarzowych.

Błąd nr 2: przechowywać steady_clock::time_point w pliku lub w modelu danych jako „moment czasu”.
steady_clock jest dobry jako wewnętrzna linijka, ale jego „wartość absolutna” nie musi odpowiadać kalendarzowi i nawet nie musi być porównywalna między uruchomieniami programu. Jeśli trzeba przechować moment w danych (a szczególnie serializować), użyj system_clock i zapisz „sekundy/milisekundy od epoki”.

Błąd nr 3: próbować mieszać time_point z różnych zegarów w arytmetyce.
Początkujący czasem oczekują, że skoro to „punkt czasu”, to można go odejmować od dowolnego innego „punktu czasu”. W std::chrono type time_point zawiera typ zegara i to jest zamierzona ochrona. Jeśli masz dwa time_point z różnych Clock, to są to dwa różne układy odniesienia i nie można ich poprawnie odjąć bez konwersji.

Błąd nr 4: wyświetlać count() bez jawnej jednostki i potem źle interpretować liczbę.
Nawet jeśli dobrze wybrałeś zegar, można się pomylić na ostatnim etapie: wypisać elapsed.count() i zapomnieć, czy to milisekundy czy mikrosekundy. Dobra praktyka — przed wyświetleniem zawsze konwertować do żądanej jednostki przez duration_cast i drukować sufiks (" ms", " us").

Błąd nr 5: „optymalizować” pomiar zbyt wcześnie, wybierając wszędzie nanosekundy.
Czasem kusi, by drukować wszystko w nanoseconds, bo „to dokładniej”. W praktyce dostaniesz duże liczby, szum pomiarów i fałszywe poczucie pewności. Dla UI i logów częściej lepsze są milisekundy, dla bardzo szybkich operacji — mikrosekundy. Najważniejsze, by jednostka odpowiadała sensowi, a nie chęci pokazania się.

1
Zadanie
C++ SELF, poziom 66, lekcja 2
Niedostępne
Kilka sekund
Kilka sekund
1
Zadanie
C++ SELF, poziom 66, lekcja 2
Niedostępne
Przesunięcie zdarzenia
Przesunięcie zdarzenia
1
Zadanie
C++ SELF, poziom 66, lekcja 2
Niedostępne
Tyki stopera
Tyki stopera
1
Zadanie
C++ SELF, poziom 66, lekcja 2
Niedostępne
Podwójny raport
Podwójny raport
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION