Конвертировать между часами C++11

Если у меня есть time_point за произвольные часы (скажем, high_resolution_clock::time_point), есть ли способ преобразовать его в time_point за другие произвольные часы (скажем, system_clock::time_point)?

Я знаю, что были бы ограничения, если бы существовала эта способность, потому что не все часы устойчивы, но есть ли какая-либо функциональность, чтобы помочь таким преобразованиям в спецификации вообще?

3 ответа

Решение

Мне было интересно, можно ли улучшить точность преобразования, предложенного ТС и Говардом Хиннантом. Для справки, вот базовая версия, которую я тестировал.

template
<
  typename DstTimePointT,
  typename SrcTimePointT,
  typename DstClockT = typename DstTimePointT::clock,
  typename SrcClockT = typename SrcTimePointT::clock
>
DstTimePointT
clock_cast_0th(const SrcTimePointT tp)
{
  const auto src_now = SrcClockT::now();
  const auto dst_now = DstClockT::now();
  return dst_now + (tp - src_now);
}

Используя тест

int
main()
{
    using namespace std::chrono;
    const auto now = system_clock::now();
    const auto steady_now = CLOCK_CAST<steady_clock::time_point>(now);
    const auto system_now = CLOCK_CAST<system_clock::time_point>(steady_now);
    const auto diff = system_now - now;
    std::cout << duration_cast<nanoseconds>(diff).count() << '\n';
}

где CLOCK_CAST было бы #defineд, пока, colck_cast_0thЯ собрал гистограмму для неработающей системы и одной под высокой нагрузкой. Обратите внимание, что это тест холодного запуска. Сначала я попытался вызвать функцию в цикле, где она дает гораздо лучшие результаты. Тем не менее, я думаю, что это создало бы ложное впечатление, потому что большинство реальных программ, вероятно, время от времени преобразуют момент времени и попадут в холодный случай.

Нагрузка была сгенерирована путем запуска следующих задач параллельно с тестовой программой. (Мой компьютер имеет четыре процессора.)

  • Тест умножения матриц (однопоточный).
  • find /usr/include -execdir grep "$(pwgen 10 1)" '{}' \; -print
  • hexdump /dev/urandom | gzip | hexdump | gzip | hexdump | gzip | hexdump | gzip | hexdump | gzip | hexdump | gzip | hexdump | gzip | hexdump | gzip | hexdump | gzip | hexdump | gzip| gunzip > /dev/null
  • dd if=/dev/urandom of=/tmp/spam bs=10 count=1000

Те команды, которые заканчивались за конечное время, выполнялись в бесконечном цикле.

Следующая гистограмма, а также последующие гистограммы показывают ошибки 50 000 прогонов с удалением наихудшего 1..

Гистограмма ошибок кругового обхода для нулевого подхода на холостом ходу

Гистограмма ошибок кругового обхода для нулевого подхода в высоко конкурентной системе

Обратите внимание, что ордината имеет логарифмическую шкалу.

Ошибки примерно попадают в диапазон от 0,5 мкс до 1,0 мкс в холостом случае и от 0,5 мкс до 1,5 мкс в предполагаемом случае.

Самое поразительное наблюдение состоит в том, что распределение ошибок далеко не симметрично (отрицательных ошибок нет вообще), что указывает на большую систематическую составляющую ошибки. Это имеет смысл, потому что если мы прервемся между двумя вызовами nowошибка всегда в одном и том же направлении, и мы не можем быть прерваны на "отрицательное количество времени".

Гистограмма для рассматриваемого случая почти выглядит как идеальное экспоненциальное распределение (следите за логарифмической шкалой!) С довольно резким срезом, который кажется правдоподобным; вероятность того, что вас прервут на время t, примерно пропорциональна e-t.

Затем я попытался использовать следующий трюк

template
<
  typename DstTimePointT,
  typename SrcTimePointT,
  typename DstClockT = typename DstTimePointT::clock,
  typename SrcClockT = typename SrcTimePointT::clock
>
DstTimePointT
clock_cast_1st(const SrcTimePointT tp)
{
  const auto src_before = SrcClockT::now();
  const auto dst_now = DstClockT::now();
  const auto src_after = SrcClockT::now();
  const auto src_diff = src_after - src_before;
  const auto src_now = src_before + src_diff / 2;
  return dst_now + (tp - src_now);
}

надеясь, что интерполяция scr_now частично отменит ошибку, вызванную неизбежным вызовом часов в последовательном порядке.

В первой версии этого ответа я утверждал, что это ничего не помогло. Оказывается, это не было правдой. После того, как Говард Хиннант указал, что он наблюдал улучшения, я улучшил свои тесты, и теперь наблюдается некоторое заметное улучшение.

Гистограмма ошибок кругового обхода для первого захода на холостую систему

Гистограмма ошибок кругового обхода для первого захода в систему с высокой конкуренцией

Это не было большим улучшением с точки зрения диапазона ошибок, однако, ошибки теперь примерно центрированы вокруг нуля, что означает, что у нас теперь есть ошибки в диапазоне от -0,5 & # 1202f; мкс до 0,5 & # 1202f; мкс., Более симметричное распределение указывает на то, что статистическая составляющая ошибки стала более доминирующей.

Затем я попытался вызвать вышеуказанный код в цикле, который выбрал бы лучшее значение для src_diff,

template
<
  typename DstTimePointT,
  typename SrcTimePointT,
  typename DstDurationT = typename DstTimePointT::duration,
  typename SrcDurationT = typename SrcTimePointT::duration,
  typename DstClockT = typename DstTimePointT::clock,
  typename SrcClockT = typename SrcTimePointT::clock
>
DstTimePointT
clock_cast_2nd(const SrcTimePointT tp,
               const SrcDurationT tolerance = std::chrono::nanoseconds {100},
               const int limit = 10)
{
  assert(limit > 0);
  auto itercnt = 0;
  auto src_now = SrcTimePointT {};
  auto dst_now = DstTimePointT {};
  auto epsilon = detail::max_duration<SrcDurationT>();
  do
    {
      const auto src_before = SrcClockT::now();
      const auto dst_between = DstClockT::now();
      const auto src_after = SrcClockT::now();
      const auto src_diff = src_after - src_before;
      const auto delta = detail::abs_duration(src_diff);
      if (delta < epsilon)
        {
          src_now = src_before + src_diff / 2;
          dst_now = dst_between;
          epsilon = delta;
        }
      if (++itercnt >= limit)
        break;
    }
  while (epsilon > tolerance);
#ifdef GLOBAL_ITERATION_COUNTER
  GLOBAL_ITERATION_COUNTER = itercnt;
#endif
  return dst_now + (tp - src_now);
}

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

Я использую следующие две простые вспомогательные функции в приведенном выше коде.

namespace detail
{

  template <typename DurationT, typename ReprT = typename DurationT::rep>
  constexpr DurationT
  max_duration() noexcept
  {
    return DurationT {std::numeric_limits<ReprT>::max()};
  }

  template <typename DurationT>
  constexpr DurationT
  abs_duration(const DurationT d) noexcept
  {
    return DurationT {(d.count() < 0) ? -d.count() : d.count()};
  }

}

Гистограмма ошибок кругового обхода для второго захода на холостую систему

Гистограмма ошибок кругового обхода для второго подхода в высоко конкурентной системе

Распределение ошибок теперь очень симметрично относительно нуля, а величина ошибки уменьшилась почти в 100 раз.

Мне было любопытно, как часто итерация будет выполняться в среднем, поэтому я добавил #ifdef к коду и #defineсделал бы это во имя глобального static переменная, что main Функция будет распечатывать. (Обратите внимание, что мы собираем два числа итераций за эксперимент, поэтому эта гистограмма имеет размер выборки 100 000.)

Гистограмма для утверждало случае, с другой стороны, кажется более равномерным. У меня нет объяснения этому, и я ожидал обратного.

Гистограмма итерационных отсчетов во втором подходе на холостом ходу

Гистограмма счетчиков итераций при втором подходе в системе с высокой конкуренцией

Как кажется, мы почти всегда достигаем лимита числа итераций (но это нормально), а иногда мы возвращаемся рано. Конечно, на форму этой гистограммы может влиять изменение значений tolerance а также limit перешел в функцию.

Наконец, я подумал, что могу быть умным и вместо того, чтобы смотреть на src_diff используйте ошибку приема-передачи напрямую в качестве критерия качества.

template
<
  typename DstTimePointT,
  typename SrcTimePointT,
  typename DstDurationT = typename DstTimePointT::duration,
  typename SrcDurationT = typename SrcTimePointT::duration,
  typename DstClockT = typename DstTimePointT::clock,
  typename SrcClockT = typename SrcTimePointT::clock
>
DstTimePointT
clock_cast_3rd(const SrcTimePointT tp,
               const SrcDurationT tolerance = std::chrono::nanoseconds {100},
               const int limit = 10)
{
  assert(limit > 0);
  auto itercnt = 0;
  auto current = DstTimePointT {};
  auto epsilon = detail::max_duration<SrcDurationT>();
  do
    {
      const auto dst = clock_cast_0th<DstTimePointT>(tp);
      const auto src = clock_cast_0th<SrcTimePointT>(dst);
      const auto delta = detail::abs_duration(src - tp);
      if (delta < epsilon)
        {
          current = dst;
          epsilon = delta;
        }
      if (++itercnt >= limit)
        break;
    }
  while (epsilon > tolerance);
#ifdef GLOBAL_ITERATION_COUNTER
  GLOBAL_ITERATION_COUNTER = itercnt;
#endif
  return current;
}

Оказывается, это была не очень хорошая идея.

Гистограмма ошибок кругового обхода для третьего подхода на холостом ходу

Гистограмма ошибок кругового обхода для первого захода в систему с высокой конкуренцией

Мы снова вернулись к несимметричному распределению ошибок, и величина ошибки также увеличилась. (Хотя функция также стала дороже!) На самом деле гистограмма для случая простоя выглядит странно. Может ли быть так, что шипы соответствуют тому, как часто мы прерываемся? Это на самом деле не имеет смысла.

Частота итераций показывает ту же тенденцию, что и раньше.

Гистограмма итерационных отсчетов в третьем подходе на простой системе

Гистограмма итерационных отсчетов в третьем подходе на простой системе

В заключение я бы порекомендовал использовать 2-й подход, и я думаю, что значения по умолчанию для необязательных параметров являются разумными, но, конечно, это может варьироваться от машины к машине. Говард Хиннант отметил, что ограничение в четыре итерации ему подходит.

Если вы реализуете это по-настоящему, вы не хотели бы упустить возможность оптимизации, чтобы проверить, std::is_same<SrcClockT, DstClockT>::value и в этом случае просто применить std::chrono::time_point_cast никогда не называя now функция (и, следовательно, не вносит ошибки).

Если вы хотите повторить мои эксперименты, я предоставлю полный код здесь. clock_castXYZ код уже завершен. (Просто объедините все примеры в один файл, #include очевидные заголовки и сохранить как clock_cast.hxx.)

Вот актуальный main.cxx что я использовал.

#include <iomanip>
#include <iostream>

#ifdef GLOBAL_ITERATION_COUNTER
static int GLOBAL_ITERATION_COUNTER;
#endif

#include "clock_cast.hxx"

int
main()
{
    using namespace std::chrono;
    const auto now = system_clock::now();
    const auto steady_now = CLOCK_CAST<steady_clock::time_point>(now);
#ifdef GLOBAL_ITERATION_COUNTER
    std::cerr << std::setw(8) << GLOBAL_ITERATION_COUNTER << '\n';
#endif
    const auto system_now = CLOCK_CAST<system_clock::time_point>(steady_now);
#ifdef GLOBAL_ITERATION_COUNTER
    std::cerr << std::setw(8) << GLOBAL_ITERATION_COUNTER << '\n';
#endif
    const auto diff = system_now - now;
    std::cout << std::setw(8) << duration_cast<nanoseconds>(diff).count() << '\n';
}

Следующие GNUmakefile строит и запускает все.

CXX = g++ -std=c++14
CPPFLAGS = -DGLOBAL_ITERATION_COUNTER=global_counter
CXXFLAGS = -Wall -Wextra -Werror -pedantic -O2 -g

runs = 50000
cutoff = 0.999

execfiles = zeroth.exe first.exe second.exe third.exe

datafiles =                            \
  zeroth.dat                           \
  first.dat                            \
  second.dat second_iterations.dat     \
  third.dat third_iterations.dat

picturefiles = ${datafiles:.dat=.png}

all: ${picturefiles}

zeroth.png: errors.gp zeroth.freq
    TAG='zeroth' TITLE="0th Approach ${SUBTITLE}" MICROS=0 gnuplot $<

first.png: errors.gp first.freq
    TAG='first' TITLE="1st Approach ${SUBTITLE}" MICROS=0 gnuplot $<

second.png: errors.gp second.freq
    TAG='second' TITLE="2nd Approach ${SUBTITLE}" gnuplot $<

second_iterations.png: iterations.gp second_iterations.freq
    TAG='second' TITLE="2nd Approach ${SUBTITLE}" gnuplot $<

third.png: errors.gp third.freq
    TAG='third' TITLE="3rd Approach ${SUBTITLE}" gnuplot $<

third_iterations.png: iterations.gp third_iterations.freq
    TAG='third' TITLE="3rd Approach ${SUBTITLE}" gnuplot $<

zeroth.exe: main.cxx clock_cast.hxx
    ${CXX} -o $@ ${CPPFLAGS} -DCLOCK_CAST='clock_cast_0th' ${CXXFLAGS} $<

first.exe: main.cxx clock_cast.hxx
    ${CXX} -o $@ ${CPPFLAGS} -DCLOCK_CAST='clock_cast_1st' ${CXXFLAGS} $<

second.exe: main.cxx clock_cast.hxx
    ${CXX} -o $@ ${CPPFLAGS} -DCLOCK_CAST='clock_cast_2nd' ${CXXFLAGS} $<

third.exe: main.cxx clock_cast.hxx
    ${CXX} -o $@ ${CPPFLAGS} -DCLOCK_CAST='clock_cast_3rd' ${CXXFLAGS} $<

%.freq: binput.py %.dat
    python $^ ${cutoff} > $@

${datafiles}: ${execfiles}
    ${SHELL} -eu run.sh ${runs} $^

clean:
    rm -f *.exe *.dat *.freq *.png

.PHONY: all clean

Вспомогательный run.sh Сценарий довольно прост. В качестве улучшения предыдущей версии этого ответа я теперь выполняю различные программы во внутреннем цикле, чтобы быть более справедливыми и, возможно, также лучше избавиться от эффектов кэширования.

#! /bin/bash -eu

n="$1"
shift

for exe in "$@"
do
    name="${exe%.exe}"
    rm -f "${name}.dat" "${name}_iterations.dat"
done

i=0
while [ $i -lt $n ]
do
    for exe in "$@"
    do
        name="${exe%.exe}"
        "./${exe}" 1>>"${name}.dat" 2>>"${name}_iterations.dat"
    done
    i=$(($i + 1))
done

И я также написал binput.py сценарий, потому что я не мог понять, как сделать гистограммы в Gnuplot в одиночку.

#! /usr/bin/python3

import sys
import math

def main():
    cutoff = float(sys.argv[2]) if len(sys.argv) >= 3 else 1.0
    with open(sys.argv[1], 'r') as istr:
        values = sorted(list(map(float, istr)), key=abs)
    if cutoff < 1.0:
        values = values[:int((cutoff - 1.0) * len(values))]
    min_val = min(values)
    max_val = max(values)
    binsize = 1.0
    if max_val - min_val > 50:
        binsize = (max_val - min_val) / 50
    bins = int(1 + math.ceil((max_val - min_val) / binsize))
    histo = [0 for i in range(bins)]
    print("minimum: {:16.6f}".format(min_val), file=sys.stderr)
    print("maximum: {:16.6f}".format(max_val), file=sys.stderr)
    print("binsize: {:16.6f}".format(binsize), file=sys.stderr)
    for x in values:
        idx = int((x - min_val) / binsize)
        histo[idx] += 1
    for (i, n) in enumerate(histo):
        value = min_val + i * binsize
        frequency = n / len(values)
        print('{:16.6e} {:16.6e}'.format(value, frequency))

if __name__ == '__main__':
    main()

Наконец, вот errors.gp...

tag = system('echo ${TAG-hist}')
file_hist = sprintf('%s.freq', tag)
file_plot = sprintf('%s.png', tag)
micros_eh = 0 + system('echo ${MICROS-0}')

set terminal png size 600,450
set output file_plot

set title system('echo ${TITLE-Errors}')

if (micros_eh) { set xlabel "error / µs" } else { set xlabel "error / ns" }
set ylabel "relative frequency"

set xrange [* : *]
set yrange [1.0e-5 : 1]

set log y
set format y '10^{%T}'
set format x '%g'

set style fill solid 0.6

factor = micros_eh ? 1.0e-3 : 1.0
plot file_hist using (factor * $1):2 with boxes notitle lc '#cc0000'

… а также iterations.gp скрипты.

tag = system('echo ${TAG-hist}')
file_hist = sprintf('%s_iterations.freq', tag)
file_plot = sprintf('%s_iterations.png', tag)

set terminal png size 600,450
set output file_plot

set title system('echo ${TITLE-Iterations}')
set xlabel "iterations"
set ylabel "frequency"

set xrange [0 : *]
set yrange [1.0e-5 : 1]

set xtics 1
set xtics add ('' 0)

set log y
set format y '10^{%T}'
set format x '%g'

set boxwidth 1.0
set style fill solid 0.6

plot file_hist using 1:2 with boxes notitle lc '#3465a4'

Нет способа сделать это точно, если вы не знаете точную разницу в длительности между двумя часами. И вы не знаете это для high_resolution_clock а также system_clock если is_same<high_resolution_clock, system_clock>{} является true,

При этом вы можете запрограммировать примерно правильный перевод, и это очень похоже на то, что говорит T.C. в своем комментарии. Действительно, libC++ играет эту хитрость в своей реализации condition_variable::wait_for:

https://github.com/llvm-mirror/libcxx/blob/master/include/__mutex_base

Призывы к now из разных часов сделаны как можно ближе друг к другу, и можно надеяться, что поток не будет прерван между этими двумя вызовами слишком долго. Это лучшее, что я знаю, и в спецификации есть место для маневра, чтобы учесть эти типы махинаций. Например, что-то разрешено просыпаться немного поздно, но не рано.

В случае с libC++ базовая ОС знает, как ждать system_clock::time_point, но спецификация говорит, что вы должны ждать steady_clock (по уважительным причинам). Так что делай, что можешь.

Вот набросок идеи HelloWorld:

#include <chrono>
#include <iostream>

std::chrono::system_clock::time_point
to_system(std::chrono::steady_clock::time_point tp)
{
    using namespace std::chrono;
    auto sys_now = system_clock::now();
    auto sdy_now = steady_clock::now();
    return time_point_cast<system_clock::duration>(tp - sdy_now + sys_now);
}

std::chrono::steady_clock::time_point
to_steady(std::chrono::system_clock::time_point tp)
{
    using namespace std::chrono;
    auto sdy_now = steady_clock::now();
    auto sys_now = system_clock::now();
    return tp - sys_now + sdy_now;
}

int
main()
{
    using namespace std::chrono;
    auto now = system_clock::now();
    std::cout << now.time_since_epoch().count() << '\n';
    auto converted_now = to_system(to_steady(now));
    std::cout << converted_now.time_since_epoch().count() << '\n';
}

Для меня, используя Apple clang/libC++ в -O3, это вывод:

1454985476610067
1454985476610073

указание на то, что объединенное преобразование имело ошибку 6 микросекунд.

Обновить

Я произвольно изменил порядок звонков now() в одном из приведенных выше преобразований, так что одно преобразование вызывает их в одном порядке, а другое вызывает их в обратном порядке. Это не должно влиять на точность любого преобразования. Однако при преобразовании обоих способов, как я делаю в этом HelloWorld, должно быть статистическое аннулирование, которое помогает уменьшить ошибку преобразования туда-обратно.

Пока у вас нет C++20, вы можете использовать это в C++17:

      std::chrono::system_clock::time_point a = std::filesystem::file_time_type::clock::to_sys(std::filesystem::file_time_type)

Возвращаемые часы из file_time_type будут зависеть от платформы, но лучше пойти этим путем, а не использовать «now()» для обоих часов для преобразования метки времени. Поскольку существует фактор, чувствительный к времени, который нежелателен в модульных тестах, переключение контекста между получением обеих временных меток «сейчас» может привести к сбою теста.

Другие вопросы по тегам