JavaRush /Курсы /C++ SELF /steady_clock vs system_clock

steady_clock vs system_clock

C++ SELF
66 уровень , 2 лекция
Открыта

1. Введение

Если вы уже ловили себя на мысли «ну время же одно, зачем столько сущностей», то поздравляю: вы мыслите как нормальный человек, а не как разработчик стандартной библиотеки. Но у компьютера «время» бывает двух разных смыслов, и эти смыслы в коде очень легко перепутать. Именно поэтому в std::chrono есть разные часы (clock): одни нужны, чтобы понимать дату/время в жизни человека, другие — чтобы честно измерять сколько длилась операция.

Представьте две ситуации.

В первой вы добавляете задачу в ваш консольный менеджер задач и хотите записать: «создано 16 января 2026, 10:15:03». Это календарное время. Ему важно совпадать с часами на компьютере, с часовым поясом пользователя, с тем, что он видит в интерфейсе.

Во второй вы хотите измерить: «сколько миллисекунд заняла сортировка списка задач» или «сколько времени ушло на чтение файла». Это измерение длительности. И тут календарность обычно вообще не нужна. Более того, она может мешать, потому что календарное время иногда скачет.

2. system_clock: календарное время, которое может прыгать

Когда программист говорит «текущее время», чаще всего он имеет в виду именно std::chrono::system_clock. Это часы, которые предназначены для календарного времени: они соответствуют тому, что показывает система (и в целом тому, что человек ожидает увидеть). У этих часов есть понятие эпохи, и их можно преобразовать в std::time_t, а значит — более-менее легко форматировать в «2026-01-16 10:15:03» (подробный вывод — это тема соседней лекции дня).

При этом у system_clock есть важная особенность: время может быть изменено извне. Например, пользователь вручную поправил часы, система синхронизировалась по NTP, произошёл переход на/с летнего времени (в некоторых конфигурациях), администратор на сервере решил «подкрутить» время. Для календаря это нормально: календарь должен отражать реальность (пусть и странную). Для измерений длительности это плохо: вы можете получить отрицательную длительность или «внезапно огромную».

Мини-идея в коде такая: system_clock хорош для «когда», но подозрителен для «сколько длилось».

Небольшой фрагмент для нашего учебного приложения TaskBook (условный менеджер задач), где мы сохраняем момент создания задачи как «миллисекунды с эпохи» — это как раз корректная роль 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();
}

Здесь важно, что мы явно приводим time_since_epoch() к миллисекундам, потому что count() сам по себе не обязан быть «в миллисекундах». (Эта осторожность — прямое продолжение темы duration_cast из прошлой лекции дня.)

3. steady_clock: часы для измерений, которые не «откатываются назад»

Теперь переходим к герою лекции — std::chrono::steady_clock. Это часы для измерения длительности. Ключевая идея steady_clock в том, что он монотонный: «сейчас» у этих часов не должно становиться меньше, чем было раньше. Проще говоря, время у steady_clock не обязано совпадать с календарём, но оно должно быть хорошей линейкой.

Когда вы измеряете скорость выполнения кода, вам важно, чтобы «старт» был раньше «финиша» (логично, да). Календарные часы могут неожиданно поехать назад, и тогда finish - start получится отрицательным. steady_clock сделан именно для того, чтобы так не происходило.

Типовой паттерн измерения выглядит скучно, но он работает годами и обычно переживает даже смену команды:

#include <chrono>

std::chrono::milliseconds measure_demo() {
    const auto start = std::chrono::steady_clock::now();
    // ... делаем работу ...
    const auto finish = std::chrono::steady_clock::now();
    return std::chrono::duration_cast<std::chrono::milliseconds>(finish - start);
}

Обратите внимание на смысловую красоту: мы вычитаем два момента (time_point), и получаем длительность (duration). Это «правильная арифметика времени», и она читается почти как формула.

4. Самое важное сравнение: «когда произошло» vs «сколько заняло»

В этом месте полезно на секунду притормозить, потому что в голове новичка эти два мира очень легко смешиваются. Если смешать — компилятор иногда вас спасёт (не всегда), а иногда вы получите баг, который проявится раз в год и строго по вторникам.

Ниже — таблица, которая фиксирует роль часов. Её стоит воспринимать как «шпаргалку на монитор», а не как теорию ради теории.

Вопрос, который вы задаёте Правильный тип часов Почему
«Который сейчас час (для человека)?»
std::chrono::system_clock
Это календарное время, привязанное к настройкам системы
«Когда задача была создана?»
std::chrono::system_clock
У события должна быть дата/время, совпадающие с реальностью
«Сколько времени заняла операция?»
std::chrono::steady_clock
Нужна монотонная линейка без скачков
«Можно ли это хранить в файле и читать завтра?» system_clock (в виде time_t или ms since epoch) У steady_clock нет смысла «в абсолютных датах»

И вот главный ориентир (без фанатизма, но как правило): таймстемпы — через system_clock, измерения — через steady_clock.

Почему нельзя сравнивать system_clock и steady_clock напрямую

Очень частая ошибка: «а давайте я возьму start от system_clock, finish от steady_clock, ну а что такого, оба же time_point». И вот тут std::chrono показывает свою суперсилу: разные Clock — разные системы отсчёта, и типы у них разные.

Даже если два значения выглядят как «точки времени», они не взаимозаменяемы. У них потенциально разная эпоха, разная природа измерения, разные гарантии. Смешивать их в выражениях — почти всегда логическая ошибка, поэтому компилятор обычно не даст вам этого сделать.

Пример того, что не нужно делать (и это хорошо, что оно не компилируется):

#include <chrono>

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

    // auto diff = b - a; // так нельзя: разные часы
}

Эта «несовместимость» — не ограничение ради издевательства. Это защита от бага, который потом очень трудно объяснить человеку, далёкому от C++: «почему время стало отрицательным?».

Почему system_clock плох для измерений

Очень хочется сказать: «ну я же могу измерить так: auto start = system_clock::now(); ... auto elapsed = now - start; — и оно покажет число». И действительно, на большинстве компьютеров в большинстве ситуаций оно покажет «что-то похожее на правду». Проблема в том, что это будет правдой ровно до того момента, пока система не решит, что время надо подправить.

Представьте сервер, который синхронизирует время по сети. Он может сделать коррекцию назад, и тогда ваша измеренная длительность внезапно станет отрицательной. Или наоборот — подправит вперёд, и вы получите «операция заняла 5 минут», хотя она заняла 20 миллисекунд. И самое неприятное: это может случиться не у вас, а у пользователя, и воспроизвести будет сложно.

Небольшой пример, который выглядит невинно, но методически неверный:

#include <chrono>

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

Он может «работать», но контракт у него слабый: вы измеряете длительность часами, которые предназначены для календаря и могут корректироваться. Поэтому в коде, который вы хотите считать надёжным, лучше сразу привыкать: измерение = steady_clock.

5. Встраиваем время в приложение TaskBook

Сейчас мы добавим две маленькие, но очень практичные детали в наш учебный консольный проект. Пусть это будет TaskBook: список задач, который мы уже умеем хранить (например, в JSON из прошлого дня) и выводить в консоль. Сегодня нас интересует не формат хранения как таковой, а что именно мы храним про время и как измеряем скорость.

Начнём с модели задачи: добавим поле created_at_ms. По смыслу это абсолютное время создания, то есть берём system_clock и сохраняем «миллисекунды с эпохи».

#include <string>

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

Теперь напишем функцию создания задачи, которая проставляет это поле. Обратите внимание: мы не храним time_point прямо в структуре, а кладём обычное число. Это проще для сериализации и для печати, и соответствует идее «на границе программы — число, внутри логики — chrono-типы».

#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;
}

Теперь добавим «измеритель» для одной операции. Например, мы хотим понять, сколько времени занимает добавление задачи (да, операция дешёвая, но это учебный пример; настоящие причины будут позже — например чтение/запись, сортировки, фильтрации).

#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);
}

А теперь — аккуратный вывод, чтобы увидеть, что происходит:

#include <iostream>

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

    auto t = make_task(1, "Прочитать про chrono");
    auto elapsed = add_task_timed(tasks, t);

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

Здесь нормально, что вы увидите 0 ms: операция слишком быстрая, и миллисекунды грубоваты. Важнее другое: вы использовали правильные часы для правильного вопроса. Если захотите точнее — можно печатать микросекунды, но это уже вопрос выбранной единицы, а не выбора часов.

6. Ментальная модель: две шкалы времени

Иногда легче всего понять разницу между steady_clock и system_clock, если представить две линейки.

Первая линейка — это календарь на стене. Сегодня 16 января 2026 года. Завтра будет 17 января. Но если кто-то возьмёт и перевесит календарь (или на телефоне поменяет дату), то «сегодня» для вас в этот момент изменится. Это неприятно, но в календарном смысле допустимо: календарь отражает соглашение.

Вторая линейка — это секундомер. Он не обязан говорить, какое сегодня число. Он обязан честно тикать вперёд, чтобы вы могли сказать: «пробежал 100 метров за 14 секунд». Если секундомер начнёт вдруг тикать назад, это перестанет быть секундомер и превратится в философский прибор, который измеряет судьбу.

Эту же мысль можно закрепить схемой:

flowchart TD
    A[Событие в мире: 'задача создана'] --> B["system_clock::now()"]
    B --> C[Храним как ms since epoch]

    D[Операция в коде: 'сортировка задач'] --> E["steady_clock::now() start"]
    E --> F[...работа...]
    F --> G["steady_clock::now() finish"]
    G --> H[elapsed = finish - start]

Если вы держите в голове эту развилку, вы почти автоматически выбираете правильные часы под задачу.

7. Типичные ошибки при сравнении steady_clock и system_clock

Ошибка №1: измерять длительность через system_clock, потому что «так проще».
Обычно это заканчивается тем, что код действительно выглядит проще… до первого странного случая на реальной машине. system_clock может корректироваться системой, поэтому измерения через него потенциально нестабильны. Для длительности используйте steady_clock, а system_clock оставляйте для таймстемпов и календарных отметок.

Ошибка №2: хранить steady_clock::time_point в файле или в модели данных как «момент времени».
steady_clock хорош как внутренняя линейка, но его «абсолютное значение» не обязано соответствовать календарю и даже не обязано быть сопоставимым между запусками программы. Если вам нужно хранить момент времени в данных (а тем более сериализовать), используйте system_clock и сохраняйте «секунды/миллисекунды с эпохи».

Ошибка №3: пытаться смешивать time_point разных часов в арифметике.
Новички иногда ожидают, что раз это «точка времени», то её можно вычитать из любой другой «точки времени». Но в std::chrono тип time_point включает в себя тип часов, и это намеренная защита. Если у вас два time_point от разных Clock, это две разные системы отсчёта, и их нельзя корректно вычитать напрямую.

Ошибка №4: выводить count() без явной единицы и потом неправильно трактовать число.
Даже если вы правильно выбрали часы, можно легко ошибиться на «последнем метре»: вывести elapsed.count() и забыть, миллисекунды это или микросекунды. Хорошая привычка — перед выводом всегда приводить к нужной единице через duration_cast и печатать суффикс (" ms", " us").

Ошибка №5: «оптимизировать» измерение слишком рано, выбирая наносекунды везде.
Иногда хочется печатать всё в nanoseconds, потому что «так точнее». На практике вы получите большие числа, шум измерений и ложную уверенность. Для UI и логов чаще подходят миллисекунды, для очень быстрых операций — микросекунды. Главное — чтобы единица соответствовала смыслу, а не желанию выглядеть круто.

1
Задача
C++ SELF, 66 уровень, 2 лекция
Недоступна
Сдвиг события
Сдвиг события
1
Задача
C++ SELF, 66 уровень, 2 лекция
Недоступна
Тики секундомера
Тики секундомера
1
Задача
C++ SELF, 66 уровень, 2 лекция
Недоступна
Двойной отчёт
Двойной отчёт
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ