在c ++ 11时钟之间转换

问题描述 投票:16回答:2

如果我有任意时钟的time_point(比如high_resolution_clock::time_point),有没有办法将它转换为time_point另一个任意时钟(比如system_clock::time_point)?

我知道如果存在这种能力,就必须有限制,因为并非所有的时钟都是稳定的,但有没有任何功能可以帮助这些规范中的转换呢?

c++ c++11 chrono
2个回答
17
投票

我想知道T.C.Howard Hinnant提出的转换的准确性是否可以提高。作为参考,这是我测试的基本版本。

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将是#defined,现在,clock_cast_0th,我收集了一个空闲系统的直方图和一个高负荷的直方图。请注意,这是一个冷启动测试。我首先尝试在循环中调用该函数,它可以提供更好的结果。但是,我认为这会产生错误的印象,因为大多数现实世界的程序可能会不时地转换一个时间点,并且会遇到冷酷的情况。

通过与测试程序并行运行以下任务来生成负载。 (我的电脑有四个CPU。)

  • 矩阵乘法基准(单线程)。
  • 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

那些将在有限时间内终止的命令在无限循环中运行。

下面的直方图 - 以及随后的直方图 - 显示50000次运行的错误,最差的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μs和1.0μs之间,在竞争情况下大约落在0.5μs和1.5μs之间。

最引人注目的观察结果是误差分布远非对称(根本没有负误差),表明误差中存在较大的系统分量。这是有道理的,因为如果我们在对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将部分取消由于不可避免地按顺序调用时钟而引入的错误。

在这个答案的第一个版本中,我声称这没有任何帮助。事实证明,事实并非如此。 Howard Hinnant指出他确实观察到了改进之后,我改进了测试,现在有了一些可观察到的改进。

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;μs到0.5&#1202f;μs的范围内存在误差。更对称的分布表明错误的统计分量变得更占优势。

接下来,我尝试在循环中调用上面的代码,为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添加到代码中,并且qazxs将其添加到#define函数将打印出来的全局static变量的名称。 (请注意,我们每个实验收集两次迭代计数,因此此直方图的样本大小为100000.)

另一方面,竞争案例的直方图似乎更加统一。我对此没有任何解释,并且预期会出现相反的情况。

main

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

看起来,我们几乎总是达到迭代次数限制(但没关系),有时我们会提前返回。这个直方图的形状当然可以通过改变传递给函数的Histogram of iteration counts in the second approach on a highly contended systemtolerance的值来影响。

最后,我认为我可以聪明,而不是看着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; }

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

总之,我建议使用第二种方法,我认为可选参数的默认值是合理的,但当然,这可能因机器而异。 Howard Hinnant评论说,只有四次迭代的限制对他来说效果很好。

如果你实现这个,你不想错过优化机会来检查Histogram of iteration counts in the third approach on an idle system和在这种情况下,只是应用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 脚本,因为我无法弄清楚如何单独使用Gnuplot中的直方图。

binput.py

最后,这是#! /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

16
投票

除非你知道两个时钟的时代之间的精确持续时间差异,否则没有办法精确地做到这一点。除非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_clockis_same<high_resolution_clock, system_clock>{}

话虽这么说,你可以编写一个近似正确的翻译,它就像true在他的评论中说的那样。实际上,libc ++在其T.C.的实现中扮演了这个伎俩:

condition_variable::wait_for

对不同时钟的https://github.com/llvm-mirror/libcxx/blob/master/include/__mutex_base#L385-L386的调用尽可能地靠近,并且希望线程不会在这两个调用之间被抢占太长时间。这是我所知道的最好的方法,并且规范在其中有摆动空间以允许这些类型的恶作剧。例如。允许一些东西醒来有点晚,但不是很早。

在libc ++的情况下,底层操作系统只知道如何等待now,但规范说你必须等待system_clock::time_point(有充分理由)。所以你尽你所能。

这是一个关于这个想法的HelloWorld草图:

steady_clock

对我来说,在-O3使用Apple clang / libc ++这个输出:

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

表示组合转换的误差为6微秒。

更新

我在上面的一个转换中随意颠倒了对1454985476610067 1454985476610073 的调用顺序,这样一个转换按一个顺序调用它们,另一个转换以相反的顺序调用它们。这应该对任何一次转换的准确性没有影响。但是,当我在这个HelloWorld中进行双向转换时,应该有统计消除,这有助于减少往返转换错误。

© www.soinside.com 2019 - 2024. All rights reserved.