【问题标题】:chrono::duration_cast problems during ratio multiplication比率乘法期间的 chrono::duration_cast 问题
【发布时间】:2018-12-15 22:12:34
【问题描述】:

我正在编写一些必须处理 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 中以同样的方式失败。简而言之,要从 system 转换为 ntp 表示,它会乘以 8388608/1953125 的比率,并在另一个方向上乘以相反的比率。它首先乘以分子,然后除以分母。数学是正确的,但初始乘法会溢出 64 位表示并被截断,尽管实际转换(除法后)仍然很容易用这些位表示。

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

  1. 这是实现中的错误,还是仅仅是限制?
  2. 如果有限制,我应该意识到这将是一个问题吗?我没有看到任何文档似乎可以让我猜到这是行不通的。
  3. 是否有一种通用的方法来实现这一点,而不会遇到同样的问题?
  4. 应该向谁报告?
  5. 最后,如果当 C++20 出现时,我使用带有手动管理的 to_sysfrom_sys 函数的 NTP 时钟,我是否可以简单地使用 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 << '\n';

  std::cout << "Naive conversion to ntp and back\n";
  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 << '\n';

  std::cout << "Broken down conversion to ntp, naive back\n";
  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 << '\n';

  std::cout << "Broken down conversion to std from ntp\n";
  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)

【问题讨论】:

    标签: c++ c++11 chrono c++20


    【解决方案1】:
    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&lt;long double&gt; 反弹。这不太容易溢出(尽管不能免疫),通常计算成本更高,并且会遇到精度问题(并且取决于numeric_limits&lt;long double&gt;::digits)。

    对我 (numeric_limits&lt;long double&gt;::digits == 64) 而言,1530996658751420125ns 的输入往返于 1530996658751420124ns(关闭 1ns)。这个算法可以通过使用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 出现时,我使用带有手动管理的 to_sysfrom_sys 函数的 NTP 时钟,我是否可以简单地使用 std::chrono::clock_cast 而不必担心其他细微的问题有问题吗?

    找出答案的一种方法是对&lt;chrono&gt; 扩展的C++20 草案规范中的this existing prototype 进行试验。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-02-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多