Skip to content

Commit ef0a902

Browse files
committed
Native (Kotlin): improve 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. * 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. * Strings are not converted between UTF-16 and UTF-8 when querying offsets for a particular timezone (the `noStringConversion` cinterop parameter handles this). * 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. After these fixes, on my machine tests run in 25 seconds instead of 56.
1 parent 4cb5717 commit ef0a902

16 files changed

+148
-65
lines changed

core/nativeMain/cinterop/cpp/cdate.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ int offset_at_datetime(const char *zone_name, int64_t epoch_sec, int *offset)
107107
if (info.second.offset.count() != *offset)
108108
*offset = info.first.offset.count();
109109
return 0;
110+
default:
111+
// the pattern matching above is supposedly exhaustive
112+
*offset = INT_MAX;
113+
return 0;
110114
}
111115
} catch (std::runtime_error e) {
112116
*offset = INT_MAX;

core/nativeMain/cinterop/date.def

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@ package = kotlinx.datetime
33
# requirements of the `date` library: https://howardhinnant.github.io/date/tz.html#Installation
44
linkerOpts.mingw_x64 = -lole32
55
linkerOpts.linux_x64 = -lpthread
6+
7+
noStringConversion = offset_at_instant offset_at_datetime

core/nativeMain/cinterop/public/cdate.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
44
*/
55
// This file specifies the native interface for datetime information queries.
6+
#pragma once
67
#include <time.h>
78
#include <stdint.h>
89
#include <stdbool.h>

core/nativeMain/cinterop/public/helper_macros.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* Copyright 2016-2020 JetBrains s.r.o.
33
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
44
*/
5+
#pragma once
56

67
/* Frees an array of allocated pointers. `array` is the array to free,
78
and `length_var` is a variable that holds the current amount of

core/nativeMain/src/Instant.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,13 @@ public actual class Instant internal constructor(internal val epochSeconds: Long
9090
(this.epochSeconds - other.epochSeconds).seconds + // won't overflow given the instant bounds
9191
(this.nanos - other.nanos).nanoseconds
9292

93-
actual override fun compareTo(other: Instant): Int =
94-
compareBy<Instant>({ it.epochSeconds }, { it.nanos }).compare(this, other)
93+
actual override fun compareTo(other: Instant): Int {
94+
val s = epochSeconds.compareTo(other.epochSeconds)
95+
if (s != 0) {
96+
return s
97+
}
98+
return nanos.compareTo(other.nanos)
99+
}
95100

96101
override fun equals(other: Any?): Boolean =
97102
this === other || other is Instant && this.epochSeconds == other.epochSeconds && this.nanos == other.nanos

core/nativeMain/src/LocalDate.kt

Lines changed: 39 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,19 @@ internal val localDateParser: Parser<LocalDate>
2323
LocalDate(year, month, day)
2424
}
2525

26-
public actual class LocalDate private constructor(actual val year: Int, actual val month: Month, actual val dayOfMonth: Int) : Comparable<LocalDate> {
26+
public actual class LocalDate actual constructor(actual val year: Int, actual val monthNumber: Int, actual val dayOfMonth: Int) : Comparable<LocalDate> {
27+
28+
init {
29+
// org.threeten.bp.LocalDate#create
30+
if (dayOfMonth > 28 && dayOfMonth > monthNumber.monthLength(isLeapYear(year))) {
31+
if (dayOfMonth == 29) {
32+
throw IllegalArgumentException("Invalid date 'February 29' as '$year' is not a leap year")
33+
} else {
34+
throw IllegalArgumentException("Invalid date '${month.name} $dayOfMonth'")
35+
}
36+
}
37+
}
38+
2739
actual companion object {
2840
actual fun parse(isoString: String): LocalDate =
2941
localDateParser.parse(isoString)
@@ -61,17 +73,6 @@ public actual class LocalDate private constructor(actual val year: Int, actual v
6173
}
6274
}
6375

64-
// org.threeten.bp.LocalDate#create
65-
actual constructor(year: Int, monthNumber: Int, dayOfMonth: Int) : this(year, Month(monthNumber), dayOfMonth) {
66-
if (dayOfMonth > 28 && dayOfMonth > month.length(isLeapYear(year))) {
67-
if (dayOfMonth == 29) {
68-
throw IllegalArgumentException("Invalid date 'February 29' as '$year' is not a leap year")
69-
} else {
70-
throw IllegalArgumentException("Invalid date '${month.name} $dayOfMonth'")
71-
}
72-
}
73-
}
74-
7576
// org.threeten.bp.LocalDate#toEpochDay
7677
internal fun toEpochDay(): Long {
7778
val y = year
@@ -96,23 +97,36 @@ public actual class LocalDate private constructor(actual val year: Int, actual v
9697

9798
internal fun withYear(newYear: Int): LocalDate = LocalDate(newYear, monthNumber, dayOfMonth)
9899

99-
actual val monthNumber: Int = month.number
100+
actual val month: Month
101+
get() = Month(monthNumber)
100102

101103
// org.threeten.bp.LocalDate#getDayOfWeek
102-
actual val dayOfWeek: DayOfWeek = run {
103-
val dow0 = floorMod(toEpochDay() + 3, 7).toInt()
104-
DayOfWeek(dow0 + 1)
105-
}
104+
actual val dayOfWeek: DayOfWeek
105+
get() {
106+
val dow0 = floorMod(toEpochDay() + 3, 7).toInt()
107+
return DayOfWeek(dow0 + 1)
108+
}
106109

107110
// org.threeten.bp.LocalDate#getDayOfYear
108-
actual val dayOfYear: Int = month.firstDayOfYear(isLeapYear(year)) + dayOfMonth - 1
109-
110-
actual override fun compareTo(other: LocalDate): Int =
111-
compareBy<LocalDate>({ it.year }, { it.monthNumber }, { it.dayOfMonth }).compare(this, other)
111+
actual val dayOfYear: Int
112+
get() = month.firstDayOfYear(isLeapYear(year)) + dayOfMonth - 1
113+
114+
// Several times faster than using `compareBy`
115+
actual override fun compareTo(other: LocalDate): Int {
116+
val y = year.compareTo(other.year)
117+
if (y != 0) {
118+
return y
119+
}
120+
val m = monthNumber.compareTo(other.monthNumber)
121+
if (m != 0) {
122+
return m
123+
}
124+
return dayOfMonth.compareTo(dayOfMonth)
125+
}
112126

113127
// org.threeten.bp.LocalDate#resolvePreviousValid
114-
private fun resolvePreviousValid(year: Int, month: Month, day: Int): LocalDate {
115-
val newDay = min(day, month.length(isLeapYear(year)))
128+
private fun resolvePreviousValid(year: Int, month: Int, day: Int): LocalDate {
129+
val newDay = min(day, month.monthLength(isLeapYear(year)))
116130
return LocalDate(year, month, newDay)
117131
}
118132

@@ -122,7 +136,7 @@ public actual class LocalDate private constructor(actual val year: Int, actual v
122136
this
123137
} else {
124138
val newYear = safeAdd(year.toLong(), yearsToAdd).toInt()
125-
resolvePreviousValid(newYear, month, dayOfMonth)
139+
resolvePreviousValid(newYear, monthNumber, dayOfMonth)
126140
}
127141

128142
// org.threeten.bp.LocalDate#plusMonths
@@ -133,7 +147,7 @@ public actual class LocalDate private constructor(actual val year: Int, actual v
133147
val monthCount: Long = year * 12L + (monthNumber - 1)
134148
val calcMonths = monthCount + monthsToAdd // safe overflow
135149
val newYear: Int = /* YEAR.checkValidIntValue( */ floorDiv(calcMonths, 12).toInt()
136-
val newMonth = Month(floorMod(calcMonths, 12).toInt() + 1)
150+
val newMonth = floorMod(calcMonths, 12).toInt() + 1
137151
return resolvePreviousValid(newYear, newMonth, dayOfMonth)
138152
}
139153

core/nativeMain/src/LocalDateTime.kt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,14 @@ public actual class LocalDateTime internal constructor(
3838
actual val second: Int get() = time.second
3939
actual val nanosecond: Int get() = time.nanosecond
4040

41-
actual override fun compareTo(other: LocalDateTime): Int =
42-
compareBy<LocalDateTime>({ it.date }, { it.time }).compare(this, other)
41+
// Several times faster than using `compareBy`
42+
actual override fun compareTo(other: LocalDateTime): Int {
43+
val d = date.compareTo(other.date)
44+
if (d != 0) {
45+
return d
46+
}
47+
return time.compareTo(other.time)
48+
}
4349

4450
override fun equals(other: Any?): Boolean =
4551
this === other || (other is LocalDateTime && compareTo(other) == 0)

core/nativeMain/src/LocalTime.kt

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,22 @@ internal class LocalTime(val hour: Int, val minute: Int, val second: Int, val na
6161
}
6262
}
6363

64-
override fun compareTo(other: LocalTime): Int =
65-
compareBy<LocalTime>({ it.hour }, { it.minute }, { it.second }, { it.nanosecond }).compare(this, other)
64+
// Several times faster than using `compareBy`
65+
override fun compareTo(other: LocalTime): Int {
66+
val h = hour.compareTo(other.hour)
67+
if (h != 0) {
68+
return h
69+
}
70+
val m = minute.compareTo(other.minute)
71+
if (m != 0) {
72+
return m
73+
}
74+
val s = second.compareTo(other.second)
75+
if (s != 0) {
76+
return s
77+
}
78+
return nanosecond.compareTo(other.nanosecond)
79+
}
6680

6781
override fun hashCode(): Int {
6882
val nod: Long = toNanoOfDay()

core/nativeMain/src/Month.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ internal fun Month.firstDayOfYear(leapYear: Boolean): Int {
2929
}
3030

3131
// From threetenbp
32-
internal fun Month.length(leapYear: Boolean): Int {
32+
internal fun Int.monthLength(leapYear: Boolean): Int {
3333
return when (this) {
34-
Month.FEBRUARY -> if (leapYear) 29 else 28
35-
Month.APRIL, Month.JUNE, Month.SEPTEMBER, Month.NOVEMBER -> 30
34+
2 -> if (leapYear) 29 else 28
35+
4, 6, 9, 11 -> 30
3636
else -> 31
3737
}
3838
}

core/nativeMain/src/TimeZone.kt

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,33 @@ package kotlinx.datetime
1010

1111
import kotlinx.cinterop.*
1212
import platform.posix.*
13+
import kotlin.native.concurrent.*
1314

1415
class DateTimeException(str: String? = null) : Exception(str)
1516

1617
public actual open class TimeZone internal constructor(actual val id: String) {
1718

19+
private val asciiName = id.cstr
20+
21+
@ThreadLocal
1822
actual companion object {
23+
24+
private var systemTimeZoneCached: TimeZone? = null
25+
1926
actual val SYSTEM: TimeZone
20-
get() = memScoped {
21-
val string = get_system_timezone() ?: throw RuntimeException("Failed to get the system timezone.")
22-
val kotlinString = string.toKString()
23-
free(string)
24-
TimeZone(kotlinString)
27+
get() {
28+
val cached = systemTimeZoneCached
29+
if (cached != null) {
30+
return cached
31+
}
32+
val systemTimeZone = memScoped {
33+
val string = get_system_timezone() ?: throw RuntimeException("Failed to get the system timezone.")
34+
val kotlinString = string.toKString()
35+
free(string)
36+
TimeZone(kotlinString)
37+
}
38+
systemTimeZoneCached = systemTimeZone
39+
return systemTimeZone
2540
}
2641

2742
actual val UTC: TimeZone = ZoneOffset.UTC
@@ -92,11 +107,11 @@ public actual open class TimeZone internal constructor(actual val id: String) {
92107

93108
actual open val Instant.offset: ZoneOffset
94109
get() {
95-
val offset = offset_at_instant(id, epochSeconds)
110+
val offset = offset_at_instant(asciiName, epochSeconds)
96111
if (offset == INT_MAX) {
97112
throw RuntimeException("Unable to acquire the offset at instant $this for zone ${this@TimeZone}")
98113
}
99-
return ZoneOffset(offset)
114+
return ZoneOffset.ofSeconds(offset)
100115
}
101116

102117
actual fun LocalDateTime.toInstant(): Instant {
@@ -105,15 +120,15 @@ public actual open class TimeZone internal constructor(actual val id: String) {
105120
}
106121

107122
internal open fun LocalDateTime.atZone(preferred: ZoneOffset? = null): ZonedDateTime = memScoped {
108-
val epochSeconds = toEpochSecond(ZoneOffset(0))
123+
val epochSeconds = toEpochSecond(ZoneOffset.UTC)
109124
val offset = alloc<IntVar>()
110125
offset.value = preferred?.totalSeconds ?: INT_MAX
111-
val transitionDuration = offset_at_datetime(id, epochSeconds, offset.ptr)
126+
val transitionDuration = offset_at_datetime(asciiName, epochSeconds, offset.ptr)
112127
if (offset.value == INT_MAX) {
113128
throw RuntimeException("Unable to acquire the offset at ${this@atZone} for zone ${this@TimeZone}")
114129
}
115130
val dateTime = this@atZone.plusSeconds(transitionDuration.toLong())
116-
ZonedDateTime(dateTime, this@TimeZone, ZoneOffset(offset.value))
131+
ZonedDateTime(dateTime, this@TimeZone, ZoneOffset.ofSeconds(offset.value))
117132
}
118133

119134
// org.threeten.bp.ZoneId#equals
@@ -128,12 +143,14 @@ public actual open class TimeZone internal constructor(actual val id: String) {
128143

129144
}
130145

131-
public actual class ZoneOffset internal constructor(actual val totalSeconds: Int, id: String? = null) : TimeZone(id
132-
?: zoneIdByOffset(totalSeconds)) {
146+
@ThreadLocal
147+
private var zoneOffsetCache: MutableMap<Int, ZoneOffset> = mutableMapOf()
148+
149+
public actual class ZoneOffset internal constructor(actual val totalSeconds: Int, id: String) : TimeZone(id) {
133150

134151
companion object {
135152
// org.threeten.bp.ZoneOffset#UTC
136-
val UTC = ZoneOffset(0)
153+
val UTC = ZoneOffset(0, "Z")
137154

138155
// org.threeten.bp.ZoneOffset#of
139156
internal fun of(offsetId: String): ZoneOffset {
@@ -219,7 +236,20 @@ public actual class ZoneOffset internal constructor(actual val totalSeconds: Int
219236
internal fun ofHoursMinutesSeconds(hours: Int, minutes: Int, seconds: Int): ZoneOffset {
220237
validate(hours, minutes, seconds)
221238
return if (hours == 0 && minutes == 0 && seconds == 0) UTC
222-
else ZoneOffset(hours * SECONDS_PER_HOUR + minutes * SECONDS_PER_MINUTE + seconds)
239+
else ofSeconds(hours * SECONDS_PER_HOUR + minutes * SECONDS_PER_MINUTE + seconds)
240+
}
241+
242+
// org.threeten.bp.ZoneOffset#ofTotalSeconds
243+
internal fun ofSeconds(seconds: Int): ZoneOffset {
244+
if (seconds % (15 * SECONDS_PER_MINUTE) == 0) {
245+
val cached = zoneOffsetCache[seconds]
246+
return if (cached != null) {
247+
cached
248+
} else {
249+
ZoneOffset(seconds, zoneIdByOffset(seconds)).also { zoneOffsetCache[seconds] = it }
250+
}
251+
}
252+
return ZoneOffset(seconds, zoneIdByOffset(seconds))
223253
}
224254

225255
// org.threeten.bp.ZoneOffset#parseNumber

core/nativeTest/src/ThreeTenBpInstantTest.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
* Copyright (c) 2007-present, Stephen Colebourne & Michael Nascimento Santos
77
*/
88

9-
package kotlinx.datetime
9+
package kotlinx.datetime.test
1010

11+
import kotlinx.datetime.*
1112
import kotlin.test.*
1213

1314
class ThreeTenBpInstantTest {

core/nativeTest/src/ThreeTenBpLocalDateTest.kt

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
* Copyright (c) 2007-present, Stephen Colebourne & Michael Nascimento Santos
77
*/
88

9-
package kotlinx.datetime
9+
package kotlinx.datetime.test
1010

11+
import kotlinx.datetime.*
1112
import kotlin.test.*
1213

1314
class ThreeTenBpLocalDateTest {
@@ -31,9 +32,9 @@ class ThreeTenBpLocalDateTest {
3132

3233
@Test
3334
fun toEpochDay() {
34-
val date_0000_01_01 = -678941 - 40587.toLong()
35+
val date_0000_01_01 = -678941 - 40587L
3536

36-
var test: LocalDate = LocalDate(0, 1, 1)
37+
var test = LocalDate(0, 1, 1)
3738
for (i in date_0000_01_01..699999) {
3839
assertEquals(i, test.toEpochDay())
3940
test = next(test)
@@ -54,10 +55,10 @@ class ThreeTenBpLocalDateTest {
5455
@Test
5556
fun dayOfWeek() {
5657
var dow = DayOfWeek.MONDAY
57-
for (month in Month.values()) {
58-
val length = month.length(false)
58+
for (month in 1..12) {
59+
val length = month.monthLength(false)
5960
for (i in 1..length) {
60-
val d = LocalDate(2007, month.number, i)
61+
val d = LocalDate(2007, month, i)
6162
assertSame(d.dayOfWeek, dow)
6263
dow = DayOfWeek(dow.number % 7 + 1)
6364
}
@@ -77,7 +78,7 @@ class ThreeTenBpLocalDateTest {
7778
val a: LocalDate = LocalDate(y, m, d)
7879
var total = 0
7980
for (i in 1 until m) {
80-
total += Month(i).length(isLeapYear(y))
81+
total += i.monthLength(isLeapYear(y))
8182
}
8283
val doy = total + d
8384
assertEquals(a.dayOfYear, doy)
@@ -178,7 +179,7 @@ class ThreeTenBpLocalDateTest {
178179
private fun next(localDate: LocalDate): LocalDate {
179180
var date = localDate
180181
val newDayOfMonth: Int = date.dayOfMonth + 1
181-
if (newDayOfMonth <= date.month.length(isLeapYear(date.year))) {
182+
if (newDayOfMonth <= date.monthNumber.monthLength(isLeapYear(date.year))) {
182183
return LocalDate(date.year, date.monthNumber, newDayOfMonth)
183184
}
184185
date = LocalDate(date.year, date.monthNumber, 1)
@@ -198,6 +199,6 @@ class ThreeTenBpLocalDateTest {
198199
if (date.month === Month.DECEMBER) {
199200
date = date.withYear(date.year - 1)
200201
}
201-
return LocalDate(date.year, date.monthNumber, date.month.length(isLeapYear(date.year)))
202+
return LocalDate(date.year, date.monthNumber, date.monthNumber.monthLength(isLeapYear(date.year)))
202203
}
203204
}

0 commit comments

Comments
 (0)