比率乘法过程中的Timo::DATION_CAST问题
我正在编写一些必须处理NTP时间的代码。我决定用std::chrono::duration
来表示它们,我希望这会让我的时间数学更容易做。
持续时间表示很简单:std::chrono::duration<std::uint64_t, std::ratio<1, INTMAX_C(0x100000000)>>
。问题出在我试图在NTP时间和系统时钟之间进行转换时。
std::chrono::system_clock::duration
是std::chrono::duration<int64_t, std::nano>
。当我尝试使用std::chrono::duration_cast
在这两个持续时间之间进行转换时,我在两个方向上都得到了大量截断。在调试器中跟踪它,我发现它与duration_cast
实现有关,它在libstdc++、libc++和Boost::chrono中以同样的方式失败。简而言之,为了从系统转换为NTP表示,它将乘以比率8388608/1953125,并在另一个方向上乘以相反的比率。它首先乘以分子,然后除以分母。数学计算是正确的,但初始乘法溢出64位表示并被截断,尽管实际转换(除法后)仍然可以很容易地用这些位表示。
我可以手动执行此转换,但我的问题如下:
- 这是实现中的错误,还是只是限制?
- 如果是限制,我是否应该意识到这将是一个问题?我没有看到任何似乎会导致能够猜测这不会起作用的文档。
- 有没有一种通用的方法来实现这一点,而不受同一问题的困扰?
- 是否应报告此情况,以及报告给谁?
- 最后,如果当C++20出现时,我使用的NTP时钟具有手动管理的
to_sys
和from_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)
解决方案
- 这是实现中的错误,还是只是限制?
只是一个限制。实施工作符合规范。
- 如果是限制,我是否应该意识到这将是一个问题?我没有看到任何似乎会导致能够猜测这不会起作用的文档。
该规范在C++标准的23.17.5.7 [time.duration.cast]中。它记录了转换算法,使其按照您的问题中所描述的那样运行。
- 有没有一种通用的方法来实现这一点,而不受同一问题的困扰?
在处理非常精细的单位或非常大的范围时,您需要注意可能出现溢出错误。chrono::duration_cast
被设计为尽可能高效地处理最常见的转换的最底层工具。chrono::duration_cast
尽可能地准确,尽可能地完全消除分歧。
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
也不能提供完美的往返。
- 是否应报告此情况,以及报告给谁?
任何人都可以通过遵循instructions at this link来针对C++标准的一半库提交缺陷报告。这样的报告将由C++标准委员会的LWG小组委员会研究。它可以被采取行动,也可以被宣布为NAD(不是缺陷)。如果问题包含建议的措辞(有关如何更改规范的详细说明),则成功的机会更高。
即,您认为标准应该怎么说?
- 最后,如果当C++20出现时,我使用的NTP时钟具有手动管理的
to_sys
和from_sys
函数,我是否就可以简单地使用std::chrono::clock_cast
,而不必担心其他微妙的问题?
找出答案的一种方法是试验<chrono>
扩展的C++20规范草案的this existing prototype。
相关文章