比率乘法过程中的Timo::DATION_CAST问题

2022-05-16 00:00:00 c++ c++11 c++20 chrono

我正在编写一些必须处理NTP时间的代码。我决定用std::chrono::duration来表示它们,我希望这会让我的时间数学更容易做。

NTP时间由无符号64位整数表示,从纪元开始的秒数在高32位,小数秒在低32位。划时代的日期是1900年1月1日,但这是一个与我在这里处理的问题不同的问题,我已经知道如何处理它。对于我正在使用的示例,假设纪元为1970年1月1日,以使计算更简单。

持续时间表示很简单:std::chrono::duration<std::uint64_t, std::ratio<1, INTMAX_C(0x100000000)>>。问题出在我试图在NTP时间和系统时钟之间进行转换时。

在我的系统上,std::chrono::system_clock::durationstd::chrono::duration<int64_t, std::nano>。当我尝试使用std::chrono::duration_cast在这两个持续时间之间进行转换时,我在两个方向上都得到了大量截断。在调试器中跟踪它,我发现它与duration_cast实现有关,它在libstdc++、libc++和Boost::chrono中以同样的方式失败。简而言之,为了从系统转换为NTP表示,它将乘以比率8388608/1953125,并在另一个方向上乘以相反的比率。它首先乘以分子,然后除以分母。数学计算是正确的,但初始乘法溢出64位表示并被截断,尽管实际转换(除法后)仍然可以很容易地用这些位表示。

我可以手动执行此转换,但我的问题如下:

  1. 这是实现中的错误,还是只是限制?
  2. 如果是限制,我是否应该意识到这将是一个问题?我没有看到任何似乎会导致能够猜测这不会起作用的文档。
  3. 有没有一种通用的方法来实现这一点,而不受同一问题的困扰?
  4. 是否应报告此情况,以及报告给谁?
  5. 最后,如果当C++20出现时,我使用的NTP时钟具有手动管理的to_sysfrom_sys函数,我是否就可以简单地使用std::chrono::clock_cast,而不必担心其他微妙的问题?

以下是我用来测试它的代码和一些示例输出:

// #define BOOST
#ifdef BOOST
#  include <boost/chrono.hpp>
#  define PREFIX boost
#else
#  include <chrono>
#  define PREFIX std
#endif
#include <cstdint>
#include <iostream>

namespace chrono = PREFIX::chrono;
using PREFIX::ratio;
using PREFIX::nano;

using ntp_dur = chrono::duration<uint64_t, ratio<1, INTMAX_C(0x100000000)>>;
using std_dur = chrono::duration<int64_t, nano>;

int main() {
  auto write = [](auto & label, auto & dur) {
    std::cout << label << ": "
              << std::dec << dur.count()
              << std::hex << " (" << dur.count() << ")" << std::endl;
  };

  auto now = chrono::system_clock::now().time_since_epoch();
  write("now", now);
  std::cout << '
';

  std::cout << "Naive conversion to ntp and back
";
  auto a = chrono::duration_cast<std_dur>(now);
  write("std", a);
  auto b = chrono::duration_cast<ntp_dur>(a);
  write("std -> ntp", b);
  auto c = chrono::duration_cast<std_dur>(b);
  write("ntp -> std", c);
  std::cout << '
';

  std::cout << "Broken down conversion to ntp, naive back
";
  write("std", a);
  auto d = chrono::duration_cast<chrono::seconds>(a);
  write("std -> sec", d);
  auto e = chrono::duration_cast<ntp_dur>(d);
  write("sec -> ntp sec", e);
  auto f = a - d;
  write("std -> std frac", f);
  auto g = chrono::duration_cast<ntp_dur>(f);
  write("std frac -> ntp frac", f);
  auto h = e + g;
  write("ntp sec + ntp frac-> ntp", h);
  auto i = chrono::duration_cast<std_dur>(h);
  write("ntp -> std", i);
  std::cout << '
';

  std::cout << "Broken down conversion to std from ntp
";
  write("ntp", h);
  auto j = chrono::duration_cast<chrono::seconds>(h);
  write("ntp -> sec", j);
  auto k = chrono::duration_cast<std_dur>(j);
  write("src -> std sec", j);
  auto l = h - j;
  write("ntp -> ntp frac", l);
  auto m = chrono::duration_cast<std_dur>(l);
  write("ntp frac -> std frac", m);
  auto n = k + m;
  write("std sec + std frac-> std", n);
}

示例输出:

now: 1530980834103467738 (153f22f506ab1eda)

Naive conversion to ntp and back
std: 1530980834103467738 (153f22f506ab1eda)
std -> ntp: 4519932809765 (41c60fd5225)
ntp -> std: 1052378865369 (f506ab1ed9)

Broken down conversion to ntp, naive back
std: 1530980834103467738 (153f22f506ab1eda)
std -> sec: 1530980834 (5b40e9e2)
sec -> ntp sec: 6575512612832804864 (5b40e9e200000000)
std -> std frac: 103467738 (62acada)
std frac -> ntp frac: 103467738 (62acada)
ntp sec + ntp frac-> ntp: 6575512613277195414 (5b40e9e21a7cdc96)
ntp -> std: 1052378865369 (f506ab1ed9)

Broken down conversion to std from ntp
ntp: 6575512613277195414 (5b40e9e21a7cdc96)
ntp -> sec: 1530980834 (5b40e9e2)
src -> std sec: 1530980834 (5b40e9e2)
ntp -> ntp frac: 444390550 (1a7cdc96)
ntp frac -> std frac: 103467737 (62acad9)
std sec + std frac-> std: 1530980834103467737 (153f22f506ab1ed9)

解决方案

  1. 这是实现中的错误,还是只是限制?

只是一个限制。实施工作符合规范。

  1. 如果是限制,我是否应该意识到这将是一个问题?我没有看到任何似乎会导致能够猜测这不会起作用的文档。

该规范在C++标准的23.17.5.7 [time.duration.cast]中。它记录了转换算法,使其按照您的问题中所描述的那样运行。

  1. 有没有一种通用的方法来实现这一点,而不受同一问题的困扰?

在处理非常精细的单位或非常大的范围时,您需要注意可能出现溢出错误。chrono::duration_cast被设计为尽可能高效地处理最常见的转换的最底层工具。chrono::duration_cast尽可能地准确,尽可能地完全消除分歧。

但是,对于任意转换,没有一种转换算法可以使用有限的存储空间始终获得正确的答案。C++17引入了三个新的转换算法,它们构建在duration_cast的基础上,旨在指导存在截断的方向:

floor  // truncate towards negative infinity
ceil   // truncate towards positive infinity
round  // truncate towards nearest, to even on tie

您可以编写自己的通用转换函数来处理困难的情况,如您所描述的情况。这种由客户提供的转换不太可能适合一般用途。例如:

template <class DOut, class Rep, class Period>
DOut
via_double(std::chrono::duration<Rep, Period> d)
{
    using namespace std::chrono;
    using dsec = duration<long double>;
    return duration_cast<DOut>(dsec{d});
}

上述示例客户端提供的转换从duration<long double>反弹。这不太容易溢出(尽管也不能幸免),通常在计算上会更昂贵,并且会受到精度问题的影响(取决于numeric_limits<long double>::digits)。

对于我(numeric_limits<long double>::digits == 64),输入1530996658751420125ns往返到1530996658751420124ns(相差1 ns)。该算法可以通过使用round来改进duration_cast(同样是以更多的计算为代价):

template <class DOut, class Rep, class Period>
DOut
via_double(std::chrono::duration<Rep, Period> d)
{
    using namespace std::chrono;
    using dsec = duration<long double>;
    return round<DOut>(dsec{d});
}

现在我的往返行程非常适合输入1530996658751420125ns。但是,如果您的long double只有53位精度,那么即使round也不能提供完美的往返。

  1. 是否应报告此情况,以及报告给谁?

任何人都可以通过遵循instructions at this link来针对C++标准的一半库提交缺陷报告。这样的报告将由C++标准委员会的LWG小组委员会研究。它可以被采取行动,也可以被宣布为NAD(不是缺陷)。如果问题包含建议的措辞(有关如何更改规范的详细说明),则成功的机会更高。

即,您认为标准应该怎么说?

  1. 最后,如果当C++20出现时,我使用的NTP时钟具有手动管理的to_sysfrom_sys函数,我是否就可以简单地使用std::chrono::clock_cast,而不必担心其他微妙的问题?

找出答案的一种方法是试验<chrono>扩展的C++20规范草案的this existing prototype。

相关文章