-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Description
Background information
For a high-performance software we had 2 important requirements:
- Add support for changing the time zone instead of using the local time zone (at least on Windows 10 >= 1903/19H1 and Windows Server >= 2022).
- Add support for dates before 1970 (the old C functions are limited to dates between 1970-01-01 00:00:00 UTC and 3000-12-31 23:59:59 UTC).
This software previously used functions like std::localtime and std::mktime that only supported the local time zone and no dates before 1970 or after 3000.
After changing from time_t to std::chrono::zoned_time and using std::chrono::time_zone (with some workarounds for Windows 10 < 1903/19H1 and Windows Server < 2022), as well as using the other C++20 chrono features, we were able to use multiple time zones, dates before 1970 and after 3000 and were even able to simplify and improve a lot of our code due to the super nice C++20 chrono features. Thank you for implementing that, it's amazing! :)
Describe the bug
After the change we started having some major performance issues. In short, the performance issues happen whenever calls to ICU.dll must be made in order to find something time zone related, such as a time zone itself or a UTC offset. Because this is something that must be done very often, it causes major performance issues.
To be specific, all calls to __std_tzdb_get_sys_info() and __std_tzdb_get_current_zone() require finding the time zone in the ICU library which seems to be very slow. To be precise, in extreme cases, __std_tzdb_get_sys_info() might use up to 98% (!) of the time of the whole CPU profiler in Visual Studio when benchmarking our software! We were able to write a wrapper and cache the current zone, but the sys_info is needed very often and is not as easy to cache, as the information in the sys_info (such as UTC offset) depends on the timestamp.
Test case
I created a new Visual Studio project, set the C++ version to C++20, added _CRT_SECURE_NO_WARNINGS (for simplicity) and ran a benchmark using the following code (in Release mode without attached debugger):
#include <chrono>
#include <iostream>
int main()
{
constexpr size_t num_benchmark_iterations = 5'000'000;
const auto now_utc = std::chrono::system_clock::now();
const auto now_utc_in_seconds = std::chrono::system_clock::to_time_t(now_utc);
const std::chrono::time_zone* current_zone = std::chrono::current_zone();
// STEP 1 - try converting using the old C functions.
const auto begin_timer_localtime = std::chrono::high_resolution_clock::now();
for (size_t n = 0; n < num_benchmark_iterations; ++n)
{
/*tm* ctime =*/ std::localtime(&now_utc_in_seconds);
}
const auto elapsed_time_localtime = std::chrono::high_resolution_clock::now() - begin_timer_localtime;
const auto elapsed_time_localtime_in_seconds = std::chrono::duration_cast<std::chrono::seconds>(elapsed_time_localtime);
const auto elapsed_time_localtime_remaining_nanoseconds = elapsed_time_localtime - elapsed_time_localtime_in_seconds;
std::cout << "Elapsed time for localtime conversion:\t" << std::format("{} {} ({})", elapsed_time_localtime_in_seconds, elapsed_time_localtime_remaining_nanoseconds, elapsed_time_localtime) << std::endl;
// STEP 2 - try converting using the modern C++ functions.
const auto begin_timer_time_zone = std::chrono::high_resolution_clock::now();
for (size_t n = 0; n < num_benchmark_iterations; ++n)
{
/*auto now_local =*/ current_zone->to_local(now_utc);
}
const auto elapsed_time_time_zone = std::chrono::high_resolution_clock::now() - begin_timer_time_zone;
const auto elapsed_time_time_zone_in_seconds = std::chrono::duration_cast<std::chrono::seconds>(elapsed_time_time_zone);
const auto elapsed_time_time_zone_remaining_nanoseconds = elapsed_time_time_zone - elapsed_time_time_zone_in_seconds;
std::cout << "Elapsed time for time zone conversion:\t" << std::format("{} {} ({})", elapsed_time_time_zone_in_seconds, elapsed_time_time_zone_remaining_nanoseconds, elapsed_time_time_zone) << std::endl;
}Results on my computer:
Elapsed time for localtime conversion: 0s 382884200ns (382884200ns)
Elapsed time for time zone conversion: 47s 964314100ns (47964314100ns)
Expected behavior
The search for time zones and conversions between sys_time and local_time should be (ideally) as fast or minimally slower than std::localtime and std::mktime.
Workarounds
- We wrote wrappers around std::chrono::current_zone() and std::chrono::default_zone(), because for our use case it was enough to query them once at startup and reuse them. (For our use case we don't have to consider the user changing the time zone in Windows during our software's runtime.)
- Call the following functions as infrequently as possible, only when absolutely necesssary:
- default constructing std::chrono::zoned_time (calls std::chrono::default_zone() each time)
- constructing std::chrono::zoned_time using a string_view for the time zone name
- constructing std::chrono::zoned_time using an std::chrono::local_time
- std::chrono::zoned_time::get_local_time()
- std::chrono::locate_zone()
- std::chrono::time_zone::get_info()
- std::chrono::time_zone::to_local()
- std::chrono::time_zone::to_sys()
-
Workaround for std::chrono::zoned_time::get_local_time(): we were able to speed things up by using std::localtime and std::mktime instead of std::chrono::time_zone whenever the local time zone is enough, so no other time zones are needed. This is something required for old versions of Windows (Windows 10 < 1903/19H1 and Windows Server < 2022), but can also be used on newer versions of Windows to speed things up. The disadvantage of this solution is that we can't use dates before 1970, so we have to choose between that and performance.
-
The last possible workaround would be to either cache the local_time or the sys_info separately for each timestamp. However, when working with lots of data, this means using additional memory (which might not always be desired), and nevertheless the local_times/sys_info still have to be queried at least once, which means that the performance issues still appear, just not as often.
Currently, std::chrono::zoned_time::get_local_time() seems to be the issue that affects us the most, although the same issue appears in all the cases listed above.
STL version
Microsoft Visual Studio Enterprise 2022 (64-bit) - Current
Version 17.2.5