Skip to content

Commit d43518d

Browse files
committed
Native (all): improve performance, fix SharedImmutable (#2)
Testing revealed that accessing even immutable global variables concurrently is not supported out-of-the-box and instead crashes the Kotlin Native runtime. Thus, `SharedImmutable` annotation was added to every global variable. Performance fixes include: * The system timezone is only queried once, after that, it's cached for the whole life of the thread; Java does something similar, as, according to https://docs.oracle.com/javase/7/docs/api/java/util/TimeZone.html#setDefault(java.util.TimeZone), the system timezone is only queried at the start of the VM, if ever. * Instead of passing strings between Kotlin and native parts, numeric ids are used. To have a sensible interpretation of such ids that prohibited their invalidation, Apple and Windows use additional memory to store the mapping. Moreover, it is assumed for Apple that the set of timezones and their rules never change, and for *nix, it is assumed that the timezone objects are always at the same memory location, which is true for now but will change if the implementation switches to use C++20 capabilities. * Most common `ZoneOffset` instances are now cached, which saves a lot of time due to not having to recompute their string representation, which is costly in aggregate due to how common getting anonymous offsets is for operations on datetimes. * Comparators have been rewritten not to use `compareBy`, which is comparatively slow. * `LocalDate` no longer uses `Month` as its main representation of months, as it is an enum, which are surprisingly slow. * `LocalDate` used to calculate many additional fields which may never be required but were nonetheless computed for each instance of the class. Now, instantiating `LocalDate` is much cheaper at the cost of several additional fields being computed on each call. * Some Kotlin-style range checks were replaced with the corresponding simple comparisons due to a missing optimization in Kotlin/Native. See #5 After these fixes, on my machine tests run in 22 seconds instead of 56.
1 parent dba92f9 commit d43518d

22 files changed

+781
-141
lines changed

core/build.gradle.kts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,13 @@ task("downloadWindowsZonesMapping") {
237237
out.println("\t{ \"$windowsName\", \"$usualName\" },")
238238
}
239239
out.println("};")
240+
out.println("""static const std::unordered_map<std::string, size_t> zone_ids = {""")
241+
var i = 0
242+
for ((usualName, windowsName) in mapping) {
243+
out.println("\t{ \"$usualName\", $i },")
244+
++i
245+
}
246+
out.println("};")
240247
}
241248
}
242249
}

core/commonMain/src/DayOfWeek.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
package kotlinx.datetime
77

8+
import kotlin.native.concurrent.*
9+
810
public expect enum class DayOfWeek {
911
MONDAY,
1012
TUESDAY,
@@ -17,6 +19,7 @@ public expect enum class DayOfWeek {
1719

1820
public val DayOfWeek.number: Int get() = ordinal + 1
1921

22+
@SharedImmutable
2023
private val allDaysOfWeek = DayOfWeek.values().asList()
2124
public fun DayOfWeek(number: Int): DayOfWeek {
2225
require(number in 1..7)

core/commonMain/src/Month.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
package kotlinx.datetime
77

8+
import kotlin.native.concurrent.*
9+
810
public expect enum class Month {
911
JANUARY,
1012
FEBRUARY,
@@ -24,7 +26,9 @@ public expect enum class Month {
2426

2527
public val Month.number: Int get() = ordinal + 1
2628

29+
@SharedImmutable
2730
private val allMonths = Month.values().asList()
31+
2832
public fun Month(number: Int): Month {
2933
require(number in 1..12)
3034
return allMonths[number - 1]

core/nativeMain/cinterop/cpp/apple.mm

Lines changed: 53 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#import <Foundation/NSDate.h>
1313
#import <Foundation/NSCalendar.h>
1414
#import <limits.h>
15+
#import <vector>
1516
#import <set>
1617
#import <string>
1718
#include "helper_macros.hpp"
@@ -29,26 +30,63 @@
2930

3031
extern "C" {
3132
#include "cdate.h"
33+
}
3234

33-
char * get_system_timezone()
35+
static std::vector<NSTimeZone *> populate()
36+
{
37+
std::vector<NSTimeZone *> v;
38+
auto names = NSTimeZone.knownTimeZoneNames;
39+
for (size_t i = 0; i < names.count; ++i) {
40+
v.push_back([NSTimeZone timeZoneWithName: names[i]]);
41+
}
42+
return v;
43+
}
44+
45+
static std::vector<NSTimeZone *> zones_cache = populate();
46+
47+
static TZID id_by_name(NSString *zone_name)
48+
{
49+
auto abbreviations = NSTimeZone.abbreviationDictionary;
50+
auto true_name = [abbreviations valueForKey: zone_name];
51+
const NSString *name = zone_name;
52+
if (true_name != nil) {
53+
name = true_name;
54+
}
55+
for (size_t i = 0; i < zones_cache.size(); ++i) {
56+
if ([name isEqualToString:zones_cache[i].name]) {
57+
return i;
58+
}
59+
}
60+
return TZID_INVALID;
61+
}
62+
63+
static NSTimeZone *timezone_by_id(TZID id)
64+
{
65+
try {
66+
return zones_cache.at(id);
67+
} catch (std::out_of_range e) {
68+
return nullptr;
69+
}
70+
}
71+
72+
extern "C" {
73+
74+
char * get_system_timezone(TZID *tzid)
3475
{
3576
CFTimeZoneRef zone = CFTimeZoneCopySystem(); // always succeeds
3677
auto name = CFTimeZoneGetName(zone);
78+
*tzid = id_by_name((__bridge NSString *)name);
3779
CFIndex bufferSize = CFStringGetLength(name) + 1;
3880
char * buffer = (char *)malloc(sizeof(char) * bufferSize);
3981
if (buffer == nullptr) {
4082
CFRelease(zone);
4183
return nullptr;
4284
}
4385
// only fails if the name is not UTF8-encoded, which is an anomaly.
44-
if (CFStringGetCString(name, buffer, bufferSize, kCFStringEncodingUTF8))
45-
{
46-
CFRelease(zone);
47-
return buffer;
48-
}
86+
auto result = CFStringGetCString(name, buffer, bufferSize, kCFStringEncodingUTF8);
87+
assert(result);
4988
CFRelease(zone);
50-
free(buffer);
51-
return nullptr;
89+
return buffer;
5290
}
5391

5492
char ** available_zone_ids()
@@ -77,25 +115,22 @@
77115
return zones_copy;
78116
}
79117

80-
int offset_at_instant(const char *zone_name, int64_t epoch_sec)
118+
int offset_at_instant(TZID zone_id, int64_t epoch_sec)
81119
{
82-
auto zone_name_nsstring = [NSString stringWithUTF8String: zone_name];
83-
auto zone = zone_by_name(zone_name_nsstring);
120+
auto zone = timezone_by_id(zone_id);
121+
if (zone == nil) { return INT_MAX; }
84122
auto date = [NSDate dateWithTimeIntervalSince1970: epoch_sec];
85123
return (int32_t)[zone secondsFromGMTForDate: date];
86124
}
87125

88-
bool is_known_timezone(const char *zone_name) {
89-
auto zone_name_nsstring = [NSString stringWithUTF8String: zone_name];
90-
return (zone_by_name(zone_name_nsstring) != nil);
126+
TZID timezone_by_name(const char *zone_name) {
127+
return id_by_name([NSString stringWithUTF8String: zone_name]);
91128
}
92129

93-
int offset_at_datetime(const char *zone_name, int64_t epoch_sec, int *offset) {
130+
int offset_at_datetime(TZID zone_id, int64_t epoch_sec, int *offset) {
94131
*offset = INT_MAX;
95-
// timezone name
96-
auto zone_name_nsstring = [NSString stringWithUTF8String: zone_name];
97132
// timezone
98-
auto zone = zone_by_name(zone_name_nsstring);
133+
auto zone = timezone_by_id(zone_id);
99134
if (zone == nil) { return 0; }
100135
/* a date in an unspecified timezone, defined by the number of seconds since
101136
the start of the epoch in *that* unspecified timezone */

core/nativeMain/cinterop/cpp/cdate.cpp

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
using namespace date;
1616
using namespace std::chrono;
1717

18+
extern "C" {
19+
#include "cdate.h"
20+
}
21+
1822
template <class T>
1923
static char * timezone_name(const T& zone)
2024
{
@@ -26,17 +30,44 @@ static char * timezone_name(const T& zone)
2630
return name_copy;
2731
}
2832

29-
extern "C" {
33+
static const time_zone *zone_by_id(TZID id)
34+
{
35+
/* The `date` library provides a linked list of `tzdb` objects. `get_tzdb()`
36+
always returns the head of that list. For now, the list never changes:
37+
a call to `reload_tzdb()` would be required to load the updated version
38+
of the timezone database. We never do this because for now (with use of
39+
`date`) this operation is not even present for the configuration that
40+
uses the system timezone database. If we move to C++20 support for this,
41+
it may be feasible to call `reload_tzdb()` and construct a more elaborate
42+
ID scheme. */
43+
auto& tzdb = get_tzdb();
44+
try {
45+
return &tzdb.zones.at(id);
46+
} catch (std::out_of_range e) {
47+
throw std::runtime_error("Invalid timezone id");
48+
}
49+
}
3050

31-
#include "cdate.h"
51+
static TZID id_by_zone(const tzdb& db, const time_zone* tz)
52+
{
53+
size_t id = tz - &db.zones[0];
54+
if (id >= db.zones.size()) {
55+
throw std::runtime_error("The time zone is not part of the tzdb");
56+
}
57+
return id;
58+
}
3259

33-
char * get_system_timezone()
60+
extern "C" {
61+
62+
char * get_system_timezone(TZID * id)
3463
{
3564
try {
3665
auto& tzdb = get_tzdb();
3766
auto zone = tzdb.current_zone();
67+
*id = id_by_zone(tzdb, zone);
3868
return timezone_name(*zone);
3969
} catch (std::runtime_error e) {
70+
*id = TZID_INVALID;
4071
return nullptr;
4172
}
4273
}
@@ -59,38 +90,35 @@ char ** available_zone_ids()
5990
}
6091
}
6192

62-
int offset_at_instant(const char *zone_name, int64_t epoch_sec)
93+
int offset_at_instant(TZID zone_id, int64_t epoch_sec)
6394
{
6495
try {
65-
auto& tzdb = get_tzdb();
6696
/* `sys_time` is usually Unix time (UTC, not counting leap seconds).
6797
Starting from C++20, it is specified in the standard. */
6898
auto stime = sys_time<std::chrono::seconds>(
6999
std::chrono::seconds(epoch_sec));
70-
auto zone = tzdb.locate_zone(zone_name);
100+
auto zone = zone_by_id(zone_id);
71101
auto info = zone->get_info(stime);
72102
return info.offset.count();
73103
} catch (std::runtime_error e) {
74104
return INT_MAX;
75105
}
76106
}
77107

78-
bool is_known_timezone(const char *zone_name)
108+
TZID timezone_by_name(const char *zone_name)
79109
{
80110
try {
81111
auto& tzdb = get_tzdb();
82-
tzdb.locate_zone(zone_name);
83-
return true;
112+
return id_by_zone(tzdb, tzdb.locate_zone(zone_name));
84113
} catch (std::runtime_error e) {
85-
return false;
114+
return TZID_INVALID;
86115
}
87116
}
88117

89-
int offset_at_datetime(const char *zone_name, int64_t epoch_sec, int *offset)
118+
int offset_at_datetime(TZID zone_id, int64_t epoch_sec, int *offset)
90119
{
91120
try {
92-
auto& tzdb = get_tzdb();
93-
auto zone = tzdb.locate_zone(zone_name);
121+
auto zone = zone_by_id(zone_id);
94122
local_seconds seconds((std::chrono::seconds(epoch_sec)));
95123
auto info = zone->get_info(seconds);
96124
switch (info.result) {
@@ -107,6 +135,10 @@ int offset_at_datetime(const char *zone_name, int64_t epoch_sec, int *offset)
107135
if (info.second.offset.count() != *offset)
108136
*offset = info.first.offset.count();
109137
return 0;
138+
default:
139+
// the pattern matching above is supposedly exhaustive
140+
*offset = INT_MAX;
141+
return 0;
110142
}
111143
} catch (std::runtime_error e) {
112144
*offset = INT_MAX;

0 commit comments

Comments
 (0)