在 C++ 中将带有时区的日期时间字符串转换为 UNIX 时间戳的快速方法
Fast way to transform datetime strings with timezones into UNIX timestamps in C++
我想用 C++ 将一个包含日期时间字符串的大文件转换为自 UNIX 纪元(1970 年 1 月 1 日)以来的秒数。我需要计算速度非常快,因为我需要处理大量的日期时间。
到目前为止,我已经尝试了两种选择。第一个是使用 time.h
中定义的 mktime。我尝试的第二个选项是 Howard Hinnant 的带有时区扩展的 date library。
这是我用来比较 mktime 和 Howard Hinnant 的 tz 性能的代码:
for( int i=0; i<RUNS; i++){
genrandomdate(&time_str);
time_t t = mktime(&time_str);
}
auto tz = current_zone()
for( int i=0; i<RUNS; i++){
genrandomdate(&time_str);
auto ymd = year{time_str.tm_year+1900}/(time_str.tm_mon+1)/time_str.tm_mday;
auto tcurr = make_zoned(tz, local_days{ymd} +
seconds{time_str.tm_hour*3600 + time_str.tm_min*60 + time_str.tm_sec}, choose::earliest);
auto tbase = make_zoned("UTC", local_days{January/1/1970});
auto dp = tcurr.get_sys_time() - tbase.get_sys_time() + 0s;
}
比较结果:
time for mktime : 0.000142s
time for tz : 0.018748s
与mktime相比,tz的性能并不好。我想要比 mktime 更快的东西,因为 mktime 在重复用于大量迭代时也非常慢。 Java Calendar 提供了一种非常快速的方法来执行此操作,但是当时区也在起作用时,我不知道任何 C++ 替代方法。
注意:Howard Hinnant 的日期在没有时区的情况下运行速度非常快(甚至超过 Java)。但这还不足以满足我的要求。
您可以采取一些措施来优化对 Howard Hinnant 的使用 date library:
auto tbase = make_zoned("UTC", local_days{January/1/1970});
时区(甚至 "UTC")的查找涉及对数据库进行二进制搜索以查找具有该名称的时区。进行一次查找并重复使用结果会更快:
// outside of loop:
auto utc_tz = locate_zone("UTC");
// inside of loop:
auto tbase = make_zoned(utc_tz, local_days{January/1/1970});
此外,我注意到 tbase
是 loop-independent,所以整个事情可以移出循环:
// outside of loop:
auto tbase = make_zoned("UTC", local_days{January/1/1970});
这里要进行进一步的小优化。变化:
auto dp = tcurr.get_sys_time() - tbase.get_sys_time() + 0s;
收件人:
auto dp = tcurr.get_sys_time().time_since_epoch();
这完全不需要 tbase
。 tcurr.get_sys_time().time_since_epoch()
是 自 1970-01-01 00:00:00 UTC 以来的持续时间,以秒为单位。秒的精度仅适用于此示例,因为输入具有秒精度。
Style nit:尽量避免在代码中加入转换因子。这意味着改变:
auto tcurr = make_zoned(tz, local_days{ymd} +
seconds{time_str.tm_hour*3600 + time_str.tm_min*60 + time_str.tm_sec}, choose::earliest);
至:
auto tcurr = make_zoned(tz, local_days{ymd} + hours{time_str.tm_hour} +
minutes{time_str.tm_min} + seconds{time_str.tm_sec},
choose::earliest);
Is there a way to avoid this binary search if this time zone is also
fixed. I mean can we get the time zone offset and DST offset and
manually adjust the time point.
如果 你不在 Windows,尝试用 -DUSE_OS_TZDB=1
编译。这使用了一个compiled-form的数据库,可以有更高的性能。
有一种方法可以获取偏移量并手动应用它 (https://howardhinnant.github.io/date/tz.html#local_info),但是除非您知道偏移量不会随 time_point
的值而变化,否则您将最终重塑 make_zoned
.
的逻辑
但是如果您确信您的 UTC 偏移量是恒定的,那么您可以这样做:
auto tz = current_zone();
// Use a sample time_point to get the utc_offset:
auto info = tz->get_info(
local_days{year{time_str.tm_year+1900}/(time_str.tm_mon+1)/time_str.tm_mday}
+ hours{time_str.tm_hour} + minutes{time_str.tm_min}
+ seconds{time_str.tm_sec});
seconds utc_offset = info.first.offset;
for( int i=0; i<RUNS; i++){
genrandomdate(&time_str);
// Apply the offset manually:
auto ymd = year{time_str.tm_year+1900}/(time_str.tm_mon+1)/time_str.tm_mday;
auto tp = sys_days{ymd} + hours{time_str.tm_hour} +
minutes{time_str.tm_min} + seconds{time_str.tm_sec} - utc_offset;
auto dp = tp.time_since_epoch();
}
更新 -- 我自己的时序测试
我是 运行 macOS 10.14.4 和 Xcode 10.2.1。我创建了一个相对安静的机器:Time machine backup is not 运行。邮件不是 运行。 iTunes 不是 运行.
我有以下应用程序,它使用几种不同的技术实现了期望转换,具体取决于预处理器设置:
#include "date/tz.h"
#include <cassert>
#include <iostream>
#include <vector>
constexpr int RUNS = 1'000'000;
using namespace date;
using namespace std;
using namespace std::chrono;
vector<tm>
gendata()
{
vector<tm> v;
v.reserve(RUNS);
auto tz = current_zone();
auto tp = floor<seconds>(system_clock::now());
for (auto i = 0; i < RUNS; ++i, tp += 1s)
{
zoned_seconds zt{tz, tp};
auto lt = zt.get_local_time();
auto d = floor<days>(lt);
year_month_day ymd{d};
auto s = lt - d;
auto h = floor<hours>(s);
s -= h;
auto m = floor<minutes>(s);
s -= m;
tm x{};
x.tm_year = int{ymd.year()} - 1900;
x.tm_mon = unsigned{ymd.month()} - 1;
x.tm_mday = unsigned{ymd.day()};
x.tm_hour = h.count();
x.tm_min = m.count();
x.tm_sec = s.count();
x.tm_isdst = -1;
v.push_back(x);
}
return v;
}
int
main()
{
auto v = gendata();
vector<time_t> vr;
vr.reserve(v.size());
auto tz = current_zone(); // Using date
sys_seconds begin; // Using date, optimized
sys_seconds end; // Using date, optimized
seconds offset{}; // Using date, optimized
auto t0 = steady_clock::now();
for(auto const& time_str : v)
{
#if 0 // Using mktime
auto t = mktime(const_cast<tm*>(&time_str));
vr.push_back(t);
#elif 1 // Using date, easy
auto ymd = year{time_str.tm_year+1900}/(time_str.tm_mon+1)/time_str.tm_mday;
auto tp = local_days{ymd} + hours{time_str.tm_hour} +
minutes{time_str.tm_min} + seconds{time_str.tm_sec};
zoned_seconds zt{tz, tp};
vr.push_back(zt.get_sys_time().time_since_epoch().count());
#elif 0 // Using date, optimized
auto ymd = year{time_str.tm_year+1900}/(time_str.tm_mon+1)/time_str.tm_mday;
auto tp = local_days{ymd} + hours{time_str.tm_hour} +
minutes{time_str.tm_min} + seconds{time_str.tm_sec};
sys_seconds zt{(tp - offset).time_since_epoch()};
if (!(begin <= zt && zt < end))
{
auto info = tz->get_info(tp);
offset = info.first.offset;
begin = info.first.begin;
end = info.first.end;
zt = sys_seconds{(tp - offset).time_since_epoch()};
}
vr.push_back(zt.time_since_epoch().count());
#endif
}
auto t1 = steady_clock::now();
cout << (t1-t0)/v.size() << " per conversion\n";
auto i = vr.begin();
for(auto const& time_str : v)
{
auto t = mktime(const_cast<tm*>(&time_str));
assert(t == *i);
++i;
}
}
对每个解决方案进行计时,然后根据基线解决方案检查其正确性。每个解决方案转换 1,000,000 个时间戳,所有时间戳在时间上都相对靠近,并输出每次转换的平均时间。
我提出了四个解决方案,以及它们在我的环境中的时间安排:
1. 使用mktime
.
输出:
3849ns per conversion
2. 以最简单的方式使用 tz.h
和 USE_OS_TZDB=0
输出:
3976ns per conversion
这比 mktime
解决方案稍慢。
3. 以最简单的方式使用 tz.h
和 USE_OS_TZDB=1
输出:
55ns per conversion
这比上面两种解决方案要快得多。但是,此解决方案在 Windows(此时)上不可用,并且在 macOS 上不支持库的闰秒部分(未在本次测试中使用)。这两个限制都是由 OS 发布时区数据库的方式造成的。
4. 以优化的方式使用 tz.h
,利用 a-priori 时间分组时间戳的知识。如果假设为假,性能会受到影响,但正确性不会受到影响。
输出:
15ns per conversion
此结果大致独立于 USE_OS_TZDB
设置。但性能依赖于输入数据不会经常更改 UTC 偏移量这一事实。该解决方案对于模糊或 non-existent 的本地时间点也很粗心。这样的本地时间点没有到 UTC 的唯一映射。如果遇到这样的本地时间点,解决方案 2 和 3 将抛出异常。
运行 时间错误 USE_OS_TZDB
OP 在 Ubuntu 上 运行 时得到了这个堆栈转储。此崩溃发生在首次访问时区数据库时。崩溃是由 OS 为 pthread 库提供的空存根函数引起的。解决方法是显式 link 到 pthreads 库(在命令行中包含 -lpthread
)。
==20645== Process terminating with default action of signal 6 (SIGABRT)
==20645== at 0x5413428: raise (raise.c:54)
==20645== by 0x5415029: abort (abort.c:89)
==20645== by 0x4EC68F6: ??? (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.25)
==20645== by 0x4ECCA45: ??? (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.25)
==20645== by 0x4ECCA80: std::terminate() (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.25)
==20645== by 0x4ECCCB3: __cxa_throw (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.25)
==20645== by 0x4EC89B8: ??? (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.25)
==20645== by 0x406AF9: void std::call_once<date::time_zone::init() const::{lambda()#1}>(std::once_flag&, date::time_zone::init() const::{lambda()#1}&&) (mutex:698)
==20645== by 0x40486C: date::time_zone::init() const (tz.cpp:2114)
==20645== by 0x404C70: date::time_zone::get_info_impl(std::chrono::time_point<date::local_t, std::chrono::duration<long, std::ratio<1l, 1l> > >) const (tz.cpp:2149)
==20645== by 0x418E5C: date::local_info date::time_zone::get_info<std::chrono::duration<long, std::ratio<1l, 1l> > >(std::chrono::time_point<date::local_t, std::chrono::duration<long, std::ratio<1l, 1l> > >) const (tz.h:904)
==20645== by 0x418CB2: std::chrono::time_point<std::chrono::_V2::system_clock, std::common_type<std::chrono::duration<long, std::ratio<1l, 1l> >, std::chrono::duration<long, std::ratio<1l, 1l> > >::type> date::time_zone::to_sys_impl<std::chrono::duration<long, std::ratio<1l, 1l> > >(std::chrono::time_point<date::local_t, std::chrono::duration<long, std::ratio<1l, 1l> > >, date::choose, std::integral_constant<bool, false>) const (tz.h:947)
==20645==
我发现 Google 的 CCTZ 可以做同样的事情。
我想用 C++ 将一个包含日期时间字符串的大文件转换为自 UNIX 纪元(1970 年 1 月 1 日)以来的秒数。我需要计算速度非常快,因为我需要处理大量的日期时间。
到目前为止,我已经尝试了两种选择。第一个是使用 time.h
中定义的 mktime。我尝试的第二个选项是 Howard Hinnant 的带有时区扩展的 date library。
这是我用来比较 mktime 和 Howard Hinnant 的 tz 性能的代码:
for( int i=0; i<RUNS; i++){
genrandomdate(&time_str);
time_t t = mktime(&time_str);
}
auto tz = current_zone()
for( int i=0; i<RUNS; i++){
genrandomdate(&time_str);
auto ymd = year{time_str.tm_year+1900}/(time_str.tm_mon+1)/time_str.tm_mday;
auto tcurr = make_zoned(tz, local_days{ymd} +
seconds{time_str.tm_hour*3600 + time_str.tm_min*60 + time_str.tm_sec}, choose::earliest);
auto tbase = make_zoned("UTC", local_days{January/1/1970});
auto dp = tcurr.get_sys_time() - tbase.get_sys_time() + 0s;
}
比较结果:
time for mktime : 0.000142s
time for tz : 0.018748s
与mktime相比,tz的性能并不好。我想要比 mktime 更快的东西,因为 mktime 在重复用于大量迭代时也非常慢。 Java Calendar 提供了一种非常快速的方法来执行此操作,但是当时区也在起作用时,我不知道任何 C++ 替代方法。
注意:Howard Hinnant 的日期在没有时区的情况下运行速度非常快(甚至超过 Java)。但这还不足以满足我的要求。
您可以采取一些措施来优化对 Howard Hinnant 的使用 date library:
auto tbase = make_zoned("UTC", local_days{January/1/1970});
时区(甚至 "UTC")的查找涉及对数据库进行二进制搜索以查找具有该名称的时区。进行一次查找并重复使用结果会更快:
// outside of loop:
auto utc_tz = locate_zone("UTC");
// inside of loop:
auto tbase = make_zoned(utc_tz, local_days{January/1/1970});
此外,我注意到 tbase
是 loop-independent,所以整个事情可以移出循环:
// outside of loop:
auto tbase = make_zoned("UTC", local_days{January/1/1970});
这里要进行进一步的小优化。变化:
auto dp = tcurr.get_sys_time() - tbase.get_sys_time() + 0s;
收件人:
auto dp = tcurr.get_sys_time().time_since_epoch();
这完全不需要 tbase
。 tcurr.get_sys_time().time_since_epoch()
是 自 1970-01-01 00:00:00 UTC 以来的持续时间,以秒为单位。秒的精度仅适用于此示例,因为输入具有秒精度。
Style nit:尽量避免在代码中加入转换因子。这意味着改变:
auto tcurr = make_zoned(tz, local_days{ymd} +
seconds{time_str.tm_hour*3600 + time_str.tm_min*60 + time_str.tm_sec}, choose::earliest);
至:
auto tcurr = make_zoned(tz, local_days{ymd} + hours{time_str.tm_hour} +
minutes{time_str.tm_min} + seconds{time_str.tm_sec},
choose::earliest);
Is there a way to avoid this binary search if this time zone is also fixed. I mean can we get the time zone offset and DST offset and manually adjust the time point.
如果 你不在 Windows,尝试用 -DUSE_OS_TZDB=1
编译。这使用了一个compiled-form的数据库,可以有更高的性能。
有一种方法可以获取偏移量并手动应用它 (https://howardhinnant.github.io/date/tz.html#local_info),但是除非您知道偏移量不会随 time_point
的值而变化,否则您将最终重塑 make_zoned
.
但是如果您确信您的 UTC 偏移量是恒定的,那么您可以这样做:
auto tz = current_zone();
// Use a sample time_point to get the utc_offset:
auto info = tz->get_info(
local_days{year{time_str.tm_year+1900}/(time_str.tm_mon+1)/time_str.tm_mday}
+ hours{time_str.tm_hour} + minutes{time_str.tm_min}
+ seconds{time_str.tm_sec});
seconds utc_offset = info.first.offset;
for( int i=0; i<RUNS; i++){
genrandomdate(&time_str);
// Apply the offset manually:
auto ymd = year{time_str.tm_year+1900}/(time_str.tm_mon+1)/time_str.tm_mday;
auto tp = sys_days{ymd} + hours{time_str.tm_hour} +
minutes{time_str.tm_min} + seconds{time_str.tm_sec} - utc_offset;
auto dp = tp.time_since_epoch();
}
更新 -- 我自己的时序测试
我是 运行 macOS 10.14.4 和 Xcode 10.2.1。我创建了一个相对安静的机器:Time machine backup is not 运行。邮件不是 运行。 iTunes 不是 运行.
我有以下应用程序,它使用几种不同的技术实现了期望转换,具体取决于预处理器设置:
#include "date/tz.h"
#include <cassert>
#include <iostream>
#include <vector>
constexpr int RUNS = 1'000'000;
using namespace date;
using namespace std;
using namespace std::chrono;
vector<tm>
gendata()
{
vector<tm> v;
v.reserve(RUNS);
auto tz = current_zone();
auto tp = floor<seconds>(system_clock::now());
for (auto i = 0; i < RUNS; ++i, tp += 1s)
{
zoned_seconds zt{tz, tp};
auto lt = zt.get_local_time();
auto d = floor<days>(lt);
year_month_day ymd{d};
auto s = lt - d;
auto h = floor<hours>(s);
s -= h;
auto m = floor<minutes>(s);
s -= m;
tm x{};
x.tm_year = int{ymd.year()} - 1900;
x.tm_mon = unsigned{ymd.month()} - 1;
x.tm_mday = unsigned{ymd.day()};
x.tm_hour = h.count();
x.tm_min = m.count();
x.tm_sec = s.count();
x.tm_isdst = -1;
v.push_back(x);
}
return v;
}
int
main()
{
auto v = gendata();
vector<time_t> vr;
vr.reserve(v.size());
auto tz = current_zone(); // Using date
sys_seconds begin; // Using date, optimized
sys_seconds end; // Using date, optimized
seconds offset{}; // Using date, optimized
auto t0 = steady_clock::now();
for(auto const& time_str : v)
{
#if 0 // Using mktime
auto t = mktime(const_cast<tm*>(&time_str));
vr.push_back(t);
#elif 1 // Using date, easy
auto ymd = year{time_str.tm_year+1900}/(time_str.tm_mon+1)/time_str.tm_mday;
auto tp = local_days{ymd} + hours{time_str.tm_hour} +
minutes{time_str.tm_min} + seconds{time_str.tm_sec};
zoned_seconds zt{tz, tp};
vr.push_back(zt.get_sys_time().time_since_epoch().count());
#elif 0 // Using date, optimized
auto ymd = year{time_str.tm_year+1900}/(time_str.tm_mon+1)/time_str.tm_mday;
auto tp = local_days{ymd} + hours{time_str.tm_hour} +
minutes{time_str.tm_min} + seconds{time_str.tm_sec};
sys_seconds zt{(tp - offset).time_since_epoch()};
if (!(begin <= zt && zt < end))
{
auto info = tz->get_info(tp);
offset = info.first.offset;
begin = info.first.begin;
end = info.first.end;
zt = sys_seconds{(tp - offset).time_since_epoch()};
}
vr.push_back(zt.time_since_epoch().count());
#endif
}
auto t1 = steady_clock::now();
cout << (t1-t0)/v.size() << " per conversion\n";
auto i = vr.begin();
for(auto const& time_str : v)
{
auto t = mktime(const_cast<tm*>(&time_str));
assert(t == *i);
++i;
}
}
对每个解决方案进行计时,然后根据基线解决方案检查其正确性。每个解决方案转换 1,000,000 个时间戳,所有时间戳在时间上都相对靠近,并输出每次转换的平均时间。
我提出了四个解决方案,以及它们在我的环境中的时间安排:
1. 使用mktime
.
输出:
3849ns per conversion
2. 以最简单的方式使用 tz.h
和 USE_OS_TZDB=0
输出:
3976ns per conversion
这比 mktime
解决方案稍慢。
3. 以最简单的方式使用 tz.h
和 USE_OS_TZDB=1
输出:
55ns per conversion
这比上面两种解决方案要快得多。但是,此解决方案在 Windows(此时)上不可用,并且在 macOS 上不支持库的闰秒部分(未在本次测试中使用)。这两个限制都是由 OS 发布时区数据库的方式造成的。
4. 以优化的方式使用 tz.h
,利用 a-priori 时间分组时间戳的知识。如果假设为假,性能会受到影响,但正确性不会受到影响。
输出:
15ns per conversion
此结果大致独立于 USE_OS_TZDB
设置。但性能依赖于输入数据不会经常更改 UTC 偏移量这一事实。该解决方案对于模糊或 non-existent 的本地时间点也很粗心。这样的本地时间点没有到 UTC 的唯一映射。如果遇到这样的本地时间点,解决方案 2 和 3 将抛出异常。
运行 时间错误 USE_OS_TZDB
OP 在 Ubuntu 上 运行 时得到了这个堆栈转储。此崩溃发生在首次访问时区数据库时。崩溃是由 OS 为 pthread 库提供的空存根函数引起的。解决方法是显式 link 到 pthreads 库(在命令行中包含 -lpthread
)。
==20645== Process terminating with default action of signal 6 (SIGABRT)
==20645== at 0x5413428: raise (raise.c:54)
==20645== by 0x5415029: abort (abort.c:89)
==20645== by 0x4EC68F6: ??? (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.25)
==20645== by 0x4ECCA45: ??? (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.25)
==20645== by 0x4ECCA80: std::terminate() (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.25)
==20645== by 0x4ECCCB3: __cxa_throw (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.25)
==20645== by 0x4EC89B8: ??? (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.25)
==20645== by 0x406AF9: void std::call_once<date::time_zone::init() const::{lambda()#1}>(std::once_flag&, date::time_zone::init() const::{lambda()#1}&&) (mutex:698)
==20645== by 0x40486C: date::time_zone::init() const (tz.cpp:2114)
==20645== by 0x404C70: date::time_zone::get_info_impl(std::chrono::time_point<date::local_t, std::chrono::duration<long, std::ratio<1l, 1l> > >) const (tz.cpp:2149)
==20645== by 0x418E5C: date::local_info date::time_zone::get_info<std::chrono::duration<long, std::ratio<1l, 1l> > >(std::chrono::time_point<date::local_t, std::chrono::duration<long, std::ratio<1l, 1l> > >) const (tz.h:904)
==20645== by 0x418CB2: std::chrono::time_point<std::chrono::_V2::system_clock, std::common_type<std::chrono::duration<long, std::ratio<1l, 1l> >, std::chrono::duration<long, std::ratio<1l, 1l> > >::type> date::time_zone::to_sys_impl<std::chrono::duration<long, std::ratio<1l, 1l> > >(std::chrono::time_point<date::local_t, std::chrono::duration<long, std::ratio<1l, 1l> > >, date::choose, std::integral_constant<bool, false>) const (tz.h:947)
==20645==
我发现 Google 的 CCTZ 可以做同样的事情。