Преобразование между С++ 11 часами

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

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

Ответы

Ответ 1

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

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 d, а пока, clock_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..

Histogram of round-trip errors for the zeroth approach on an idle system

Histogram of round-trip errors for the zeroth approach on a highly contended system

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

Ошибки примерно попадают в диапазон от 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 частично отменит ошибку, scr_now неизбежным вызовом часов в последовательном порядке.

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

Histogram of round-trip errors for the first approach on an idle system

Histogram of round-trip errors for the first approach on a highly contended system

Это не было большим улучшением с точки зрения диапазона ошибок, однако, ошибки теперь примерно центрированы вокруг нуля, что означает, что у нас теперь есть ошибки в диапазоне от -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()};
  }

}

Histogram of round-trip errors for the second approach on an idle system

Histogram of round-trip errors for the second approach on a highly contended system

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

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

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

Histogram of iteration counts in the second approach on an idle system

Histogram of iteration counts in the second approach on a highly contended system

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

Наконец, я подумал, что могу быть умным и вместо того, чтобы смотреть на src_diff использовать ошибку 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;
}

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

Histogram of round-trip errors for the third approach on an idle system

Histogram of round-trip errors for the first approach on a highly contended system

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

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

Histogram of iteration counts in the third approach on an idle system

Histogram of iteration counts in the third approach on an idle system

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

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

Если вы хотите повторить мои эксперименты, я предоставлю полный код здесь. clock_cast XYZ код clock_cast XYZ уже завершен. (Просто объедините все примеры в один файл, #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 [email protected] ${CPPFLAGS} -DCLOCK_CAST='clock_cast_0th' ${CXXFLAGS} $<

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

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

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

%.freq: binput.py %.dat
    python $^ ${cutoff} > [email protected]

${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 "[email protected]"
do
    name="${exe%.exe}"
    rm -f "${name}.dat" "${name}_iterations.dat"
done

i=0
while [ $i -lt $n ]
do
    for exe in "[email protected]"
    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'

Ответ 2

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

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

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

Вызовы 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, должно быть статистическое аннулирование, которое помогает уменьшить ошибку преобразования туда-обратно.