【问题标题】:Convert between c++11 clocks在 c++11 时钟之间转换
【发布时间】:2016-05-18 20:42:59
【问题描述】:

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

我知道,如果存在这种能力,肯定会有限制,因为并非所有时钟都是稳定的,但规范中是否有任何功能可以帮助进行此类转换?

【问题讨论】:

  • 标记 [chrono] 以吸引 Howard Hinnant,但我能想到的唯一(近似)方法是取一个时钟的 now(),从时间点中减去它,然后添加结果 @ 987654327@ 到另一个时钟的now()。显然这并不准确(因为两个now()s 不太可能代表完全相同的时间点),但可能足够接近。
  • 我太能预测了... ;-)
  • 这个问题值得被接受,我的投票是 5gon12eder 的。这是一个值得研究的答案,它提供了如此多的信息和可能性。他的答案是基于我的,所以我不想删除我的(让读者了解背景)。
  • @HowardHinnant 我肯定会接受答案。你们很难决定选择哪一个,所以我一直在给 SE 一些时间投票 =)
  • 霍华德的回答正是 gcc 的 condition_variable::wait_until() 现在所做的,而且大部分时间都很好。但是,time_point 范围受表示数据类型的限制,因此此转换仅适用于两个时钟上的 time_points 重叠的范围。使用非常远的 time_points 时必须非常小心,尤其是 time_point::max()。见gcc.gnu.org/bugzilla/show_bug.cgi?id=58931

标签: c++ c++11 chrono


【解决方案1】:

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

负载是通过与测试程序并行运行以下任务生成的。 (我的电脑有四个 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 &gt; /dev/null
  • dd if=/dev/urandom of=/tmp/spam bs=10 count=1000

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

以下直方图 - 以及随后的直方图 - 显示了 50000 次运行的错误,其中最差的 1‰ 被移除。

请注意,纵坐标是对数刻度。

空闲情况下的误差大致在 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 指出他确实观察到了改进之后,我改进了我的测试,现在有了一些明显的改进。

在误差范围方面并没有太大的改进,但是,误差现在大致集中在零附近,这意味着我们现在的误差范围从 -0.5Ҳf;µs 到 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 添加到代码中,并将#defined 添加到main 函数将打印出的全局static 变量的名称中。 (请注意,我们每个实验收集两个迭代计数,因此该直方图的样本大小为 100000。)

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

看起来,我们几乎总是达到迭代次数限制(但这没关系),有时我们确实会提前返回。这个直方图的形状当然可以通过改变传递给函数的tolerancelimit 的值来影响。

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

事实证明这不是一个好主意。

我们再次回到非对称误差分布,误差的幅度也增加了。 (虽然该函数也变得更加昂贵!)实际上,空闲情况的直方图看起来奇怪。难道尖峰对应于我们被打断的频率吗?这实际上没有意义。

迭代频率呈现出与之前相同的趋势。

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

如果你真的实现了这个,你不想错过检查std::is_same&lt;SrcClockT, DstClockT&gt;::value的优化机会,在这种情况下,只需应用std::chrono::time_point_cast而不调用任何now函数(因此不会引入错误)。

如果您想重复我的实验,我将在此处提供完整代码。 clock_cast<i>XYZ</i> 代码已经完成。 (只需将所有示例连接到一个文件中,#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'

【讨论】:

  • 不错的分析!我很惊讶你的第一个技术没有显示出任何改进。
  • Fwiw,我在我的系统上实现并测量了你的第一个技术(通过 3 次调用 now()),它确实将我系统上的往返错误减少了两倍。您在我的系统上使用的第二种技术将错误减少了 10 倍。我发现 limit = 4tolerance = 100ns 很好。
  • @HowardHinnant 你说得对,谢谢。我一开始没有正确测量这个。我现在改进了我的测试并相应地更新了我的答案。我无法准确复制您的数字,但我可以确认总体趋势。
  • 惊人的答案。您创建max_duration 函数而不是使用SrcDurationT::max() 的任何原因?
  • 认真的吗?没有人考虑标准中的时钟间可转换性?他们真的希望我们按照上面的方法检查文件的最后写入时间,比如挂钟吗?哎呀...
【解决方案2】:

除非您知道两个时钟时期之间的精确持续时间差,否则无法精确地做到这一点。你不知道high_resolution_clocksystem_clock 的这一点,除非is_same&lt;high_resolution_clock, system_clock&gt;{}true

话虽如此,您可以编写一个大致正确的翻译,它就像T.C. 在他的评论中所说的那样。事实上,libc++ 在其condition_variable::wait_for 的实现中使用了这个技巧:

https://github.com/llvm-mirror/libcxx/blob/78d6a7767ed57b50122a161b91f59f19c9bd0d19/include/__mutex_base#L455

对不同时钟的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';
}

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

1454985476610067
1454985476610073

表示合并转换有6微秒的误差。

更新

我在上述转换之一中任意颠倒了对now() 的调用顺序,这样一个转换以一种顺序调用它们,而另一种则以相反的顺序调用它们。这应该对任何一次转换的准确性没有影响。但是,当像我在这个 HelloWorld 中那样转换两种方式时,应该有一个统计取消,这有助于减少往返转换错误。

【讨论】:

  • 我为您建议的转换编写了一个测试,以调查该方法的准确性,并以额外的时钟调用为代价得出了一个更准确的版本。希望你觉得这很有趣。
  • 为什么不在标准库中加入一个 Epoch 概念呢? time_point 不需要知道任何时钟,它不需要知道如何创建更多 time_point。 time_point 应该只知道 Epoch,每个时钟都应该产生具有某个 epoch 的 time_points。然后,两个具有相同 Epochs 的时钟可以直接比较它们的测量值,这是它们应该能够做到的(当然,这可能会根据时钟误差产生令人惊讶的结果,但是将时钟测量值与其自身进行比较也是如此,例如不稳定时钟)。
  • 必须根据 UTC 等时间标准来定义纪元。对于像steady_clock 这样的东西,这在某些平台上可能会变得不切实际。 steady_clock 通常只是自启动以来的 CPU 周期数。它根本不必与 UTC 相关。因此,最好的办法是询问每个时钟的时代回溯多远,减去这两个结果以获得持续时间,即时代差异,然后使用该差异进行转换。
  • 首先,感谢@HowardHinnant 的回答。我用它在 C++17 中将file_clock(从last_write_time)转换为system_clock。其次,我在测试过程中遇到了问题,即同一个未修改的文件可能会产生稍微不同的 system_clock 时间戳,这迫使我将单元测试更改为有一个捏造因素。
  • 对于file_clock,每个平台都应该有一个确定的纪元。我相信对于 Apple 和 Linux 操作系统来说,那个时代将与 system_clock: 1970-01-01 00:00:00 UTC 相同(不包括闰秒)。对于 Windows:1601-01-01 00:00:00 UTC,不包括闰秒。有了这样的已知时期,您只需添加/减去时期的差异即可在这两个时钟之间进行转换。例如。 Windows 偏移量为 11644473600s。
猜你喜欢
  • 1970-01-01
  • 2019-10-01
  • 2016-08-06
  • 2017-03-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-10-28
相关资源
最近更新 更多