我正在编写一些必须处理NTP时间的代码。我决定使用std::chrono::duration<std::uint64_t, std::ratio<1, INTMAX_C(0x100000000)>>
代表他们,希望这可以使我的时间数学更容易实现。
NTP时间由一个无符号的64位整数表示,从纪元以来的秒数在高32位中,小数秒在低32位中。纪元日期是1900年1月1日,但这是一个与我在这里处理的问题不同的问题,我已经知道如何处理。对于我正在处理的示例,假设纪元是1970年1月1日,以使数学更简单。
持续时间表示很简单:std::chrono::system_clock::duration
。问题出在我尝试在NTP时间和系统时钟之间进行转换时。
在我的系统上,std::chrono::duration<int64_t, std::nano>
是std::chrono::duration_cast
。当我尝试使用duration_cast
在这两个持续时间之间进行转换时,我在任一方向都被截断。在调试器中跟踪此错误,发现它与to_sys
实现有关,该实现在libstdc ++,libc ++和boost :: chrono中以相同的方式失败。简而言之,要从系统转换为ntp表示,它要乘以8388608/1953125的比率,然后乘以另一个方向的反向比率。它首先乘以分子,然后除以分母。数学上是正确的,但是初始乘法会溢出64位表示并被截断,尽管实际转换(后除法)仍然很容易在这些位中表示。
我可以手动进行此转换,但是我的问题如下:
from_sys
和std::chrono::clock_cast
功能的NTP时钟,我能否简单地使用// #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)
示例输出:
{{1}}
答案 0 :(得分:3)
- 这是实现中的错误还是仅仅是一个限制?
只是一个限制。根据规范的实现。
- 如果有限制,我是否应该意识到这是一个问题?我没有看到任何文件似乎可以导致猜测这行不通。
该规范在C ++标准的23.17.5.7 [time.duration.cast]中。它记录了转换算法的行为,如您在问题中所述。
- 是否存在一种通用的方法来实现此目标,而不会成为同一问题的猎物?
在处理非常精细的单位或非常大的范围时,您需要意识到潜在的溢出错误。 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
的往返行程(相差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
也无法提供理想的往返行程。
- 这应该向谁报告吗?
任何人都可以按照instructions at this link提交针对C ++标准库一半的缺陷报告。 C ++标准委员会的LWG小组委员会将研究此类报告。它可能已被执行或被声明为NAD(不是缺陷)。如果某个问题包含建议的措辞(有关如何更改规范的详细说明),则成功的机会更大。
即您认为该标准应该怎么说?
- 最后,如果当C ++ 20出现时,我使用具有手动管理的
to_sys
和from_sys
功能的NTP时钟,我能否简单地使用std::chrono::clock_cast
而不能还要担心其他细微的问题?
一种发现的方法是尝试使用C <chrono>
扩展的C ++ 20规范草案的this existing prototype。