Почему std::chrono::time_point недостаточно велик для хранения struct timespec?

Я пробую недавний std::chrono API и обнаружил, что в 64-битной архитектуре Linux и компиляторе gcc классы time_point и duration не могут обрабатывать максимальный диапазон времени операционной системы при максимальном разрешении (наносекунды). На самом деле кажется, что хранилище для этих классов представляет собой 64-битный целочисленный тип, по сравнению с timespec и timeval, которые внутренне используют два 64-битных целых числа, одно для секунд и одно для наносекунд:

#include <iostream>
#include <chrono>
#include <typeinfo>
#include <time.h>

using namespace std;
using namespace std::chrono;

int main()
{
    cout << sizeof(time_point<nanoseconds>) << endl;                       // 8
    cout << sizeof(time_point<nanoseconds>::duration) << endl;             // 8
    cout << sizeof(time_point<nanoseconds>::duration::rep) << endl;        // 8
    cout << typeid(time_point<nanoseconds>::duration::rep).name() << endl; // l
    cout << sizeof(struct timespec) << endl;                               // 16
    cout << sizeof(struct timeval) << endl;                                // 16
    return 0;
}

В 64-битной Windows (MSVC2017) ситуация очень похожа: тип хранилища также является 64-битным целым числом. Это не проблема при работе с устойчивыми (монотонными) часами, но ограничения хранилища делают различные реализации API неподходящими для хранения больших дат и более широких временных интервалов, создавая почву для ошибок, подобных Y2K. Проблема признана? Есть ли планы по улучшению реализации или улучшению API?


person ceztko    schedule 30.05.2017    source источник


Ответы (2)


Это было сделано для того, чтобы вы получили максимальную гибкость наряду с компактными размерами. Если вам нужна сверхвысокая точность, вам обычно не нужен очень большой диапазон. И если вам нужен очень большой диапазон, вам обычно не нужна очень высокая точность.

Например, если вы торгуете наносекундами, нужно ли вам постоянно думать о большем, чем +/- 292 года? И если вам нужно подумать о большем диапазоне, микросекунды дают вам +/- 292 тысячи лет.

MacOS system_clock фактически возвращает микросекунды, а не наносекунды. Так что часы могут идти 292 тысячи лет с 1970 года, пока не переполнятся.

Windows system_clock имеет точность в 100 нс и, следовательно, имеет диапазон +/- 29,2 тысячи лет.

Если пары сотен тысяч лет все еще недостаточно, попробуйте миллисекунды. Теперь вы находитесь в диапазоне +/- 292 миллиона лет.

Наконец, если вам просто нужно иметь наносекундную точность более пары сотен лет, <chrono> также позволяет настроить хранилище:

using dnano = duration<double, nano>;

Это дает вам наносекунды, сохраненные как double. Если ваша платформа поддерживает 128-битный интегральный тип, вы также можете использовать его:

using big_nano = duration<__int128_t, nano>;

Черт возьми, если вы пишете перегруженные операторы для timespec, вы даже можете использовать это для хранения (хотя я этого не рекомендую).

Вы также можете достичь точности выше наносекунд, но при этом вы пожертвуете диапазоном. Например:

using picoseconds = duration<int64_t, pico>;

Это имеет диапазон всего +/- 0,292 года (несколько месяцев). Так что вы действительно должны быть осторожны с этим. Однако отлично подходит для синхронизации, если у вас есть исходные часы, которые дают вам точность менее наносекунды.

Посмотрите этот видеоролик, чтобы узнать больше о <chrono>.

Для создания, управления и хранения дат с диапазоном, превышающим срок действия текущего григорианского календаря, я создал этот библиотека дат с открытым исходным кодом, которая расширяет библиотеку <chrono> календарными службами. Эта библиотека хранит год в виде 16-битного целого числа со знаком, поэтому имеет диапазон +/- 32 000 лет. Его можно использовать следующим образом:

#include "date.h"

int
main()
{
    using namespace std::chrono;
    using namespace date;
    system_clock::time_point now = sys_days{may/30/2017} + 19h + 40min + 10s;
}

Обновить

В комментариях ниже задается вопрос, как «нормализовать» duration<int32_t, nano> в секунды и наносекунды (а затем добавить секунды к time_point).

Во-первых, я бы поостерегся запихивать наносекунды в 32 бита. Диапазон составляет чуть более +/- 2 секунды. Но вот как я выделяю такие единицы:

    using ns = duration<int32_t, nano>;
    auto n = ns::max();
    auto s = duration_cast<seconds>(n);
    n -= s;

Обратите внимание, что это работает, только если n положительное. Чтобы правильно обрабатывать отрицательные n, лучше всего сделать следующее:

    auto n = ns::max();
    auto s = floor<seconds>(n);
    n -= s;

std::floor представлен в C++17. Если вы хотите получить его раньше, вы можете получить его здесь или здесь.

Я неравнодушен к описанной выше операции вычитания, так как считаю ее более читабельной. Но это тоже работает (если n не отрицательное):

    auto s = duration_cast<seconds>(n);
    n %= 1s;

1s появился в C++14. В C++11 вместо этого вам придется использовать seconds{1}.

Если у вас есть секунды (s), вы можете добавить их к своим time_point.

person Howard Hinnant    schedule 30.05.2017
comment
Ваш ответ имеет большой смысл, спасибо. Есть ли в API некоторые товары для нормализации, например, time_point<days> и duration<int32_t, nano>, используемые вместе? Так, например, никогда не бывает что-то = 1 секунда в продолжительности? - person ceztko; 30.05.2017
comment
@ceztko: Извините, я не совсем понимаю вопрос. Если следующее не отвечает на него, не могли бы вы перефразировать? time_point<days> + duration<int32_t, nano> даст вам time_point<int32_t, nano> (которое переполнится через 2 секунды). Встроенной защиты от переполнения нет. Однако, если вы возьмете библиотеку safeint, вы можете использовать ее для представления, чтобы получить защиту от переполнения. - person Howard Hinnant; 30.05.2017
comment
Извините, пример был на самом деле неправильным. На самом деле я имел в виду нормализовать time_point<seconds> и duration<int32_t, nano>: в этом случае я, вероятно, преобразовал бы duration<int32_t, nano> в секунды (взяв слово) и суммировал бы в time_point<seconds> и переписал продолжительность по модулю одна секунда. Просто интересно, какой самый быстрый способ сделать это с помощью API. - person ceztko; 30.05.2017
comment
Я добавлю это к ответу. - person Howard Hinnant; 30.05.2017

std::chrono::nanoseconds — это псевдоним типа для std::chrono::duration<some_t, std::nano>, где some_t — это целое число со знаком с хранилищем не менее 64 бит. Это по-прежнему обеспечивает диапазон не менее 292 лет с точностью до наносекунды.

Примечательно, что единственными целочисленными типами с такими характеристиками, упомянутыми в стандарте, являются семейства int(|_fast|_least)64_t.

Вы можете выбрать более широкий тип для представления своего времени, если он предусмотрен в вашей реализации. Кроме того, вы можете предоставить пространство имен с набором определений типов, которые отражают отношения std::chrono, с вашим более широким типом в качестве представления.

person Caleth    schedule 30.05.2017