Skip to content

Commit 780ee8b

Browse files
authored
Lazily calculate needed components in dateComponents(:from:in:) (#1620)
We are currently eagerly calculating all components in `_GregorianCalendar.dateComponents(:from:in:)` even if they're not needed. Skip all those if they're not being requested at all. Also switch from floating point's `remainder(dividingBy:)` and `truncatingRemainder(dividingBy:)` to integer math. The use case in the original report is essentially the `NextDatesMatchingOnHour` benchmark, where only time components are requested. Here's the result for this one. For other existing benchmarks I'm seeing 1.5-2x of improvement. ``` // NextDatesMatchingOnHour ** BEFORE ** ╒═══════════════════════════╤═════════╤═════════╤═════════╤═════════╤═════════╤═════════╤═════════╤═════════╕ │ Metric │ p0 │ p25 │ p50 │ p75 │ p90 │ p99 │ p100 │ Samples │ ╞═══════════════════════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════╡ │ Malloc (total) * │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 28 │ ├───────────────────────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤ │ Throughput (# / s) (K) │ 9 │ 9 │ 9 │ 9 │ 9 │ 9 │ 9 │ 28 │ ├───────────────────────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤ │ Time (total CPU) (μs) * │ 108 │ 109 │ 109 │ 109 │ 109 │ 110 │ 110 │ 28 │ ╘═══════════════════════════╧═════════╧═════════╧═════════╧═════════╧═════════╧═════════╧═════════╧═════════╛ ** AFTER ** ╒═══════════════════════════╤═════════╤═════════╤═════════╤═════════╤═════════╤═════════╤═════════╤═════════╕ │ Metric │ p0 │ p25 │ p50 │ p75 │ p90 │ p99 │ p100 │ Samples │ ╞═══════════════════════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════╡ │ Malloc (total) * │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 97 │ ├───────────────────────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤ │ Throughput (# / s) (K) │ 33 │ 33 │ 32 │ 32 │ 32 │ 31 │ 31 │ 97 │ ├───────────────────────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤ │ Time (total CPU) (μs) * │ 30 │ 31 │ 31 │ 31 │ 31 │ 31 │ 31 │ 97 │ ╘═══════════════════════════╧═════════╧═════════╧═════════╧═════════╧═════════╧═════════╧═════════╧═════════╛ ``` Fixes 165794940
1 parent 7a27580 commit 780ee8b

File tree

3 files changed

+39
-13
lines changed

3 files changed

+39
-13
lines changed

Benchmarks/Benchmarks/Internationalization/BenchmarkCalendar.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,23 @@ func calendarBenchmarks() {
136136
}
137137
}
138138

139+
let testDates = {
140+
let date = Date(timeIntervalSince1970: 0)
141+
var dates = [Date]()
142+
dates.reserveCapacity(10000)
143+
for i in 0...10000 {
144+
dates.append(Date(timeInterval: Double(i * 3600), since: date))
145+
}
146+
return dates
147+
}()
148+
149+
Benchmark("NextDatesMatchingOnHour") { _ in
150+
for d in testDates {
151+
let t = currentCalendar.nextDate(after: d, matching: DateComponents(minute: 0, second: 0), matchingPolicy: .nextTime)
152+
blackHole(t)
153+
}
154+
}
155+
139156
// MARK: - Allocations
140157
let reference = Date(timeIntervalSince1970: 1474666555.0) //2016-09-23T14:35:55-0700
141158

Sources/FoundationEssentials/Calendar/Calendar.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,10 @@ public struct Calendar : Hashable, Equatable, Sendable {
330330
// The calendar and timeZone properties do not count as a 'highest unit set', since they are not ordered in time like the others are.
331331
return nil
332332
}
333+
334+
var containsOnlyTimeComponents: Bool {
335+
!self.contains(.era) && !self.contains(.year) && !self.contains(.dayOfYear) && !self.contains(.quarter) && !self.contains(.month) && !self.contains(.day) && !self.contains(.weekday) && !self.contains(.weekdayOrdinal) && !self.contains(.weekOfMonth) && !self.contains(.weekOfYear) && !self.contains(.yearForWeekOfYear) && !self.contains(.isLeapMonth) && !self.contains(.isRepeatedDay)
336+
}
333337
}
334338

335339
/// An enumeration for the various components of a calendar date.

Sources/FoundationEssentials/Calendar/Calendar_Gregorian.swift

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1991,22 +1991,26 @@ internal final class _CalendarGregorian: _CalendarProtocol, @unchecked Sendable
19911991
let dateOffsetInSeconds = localDate.timeIntervalSinceReferenceDate.rounded(.down)
19921992
let date = Date(timeIntervalSinceReferenceDate: dateOffsetInSeconds) // Round down the given date to seconds
19931993

1994-
let useJulianRef = useJulianReference(date)
1994+
let totalSeconds = Int(dateOffsetInSeconds)
1995+
let secondsInDay = (totalSeconds % 86400 + 86400) % 86400
19951996

1996-
var timeInDay = dateOffsetInSeconds.remainder(dividingBy: 86400) // this has precision of one second
1997-
if (timeInDay < 0) {
1998-
timeInDay += 86400
1999-
}
2000-
2001-
let hour = Int(timeInDay / 3600) // zero-based
2002-
timeInDay = timeInDay.truncatingRemainder(dividingBy: 3600.0)
2003-
2004-
let minute = Int(timeInDay / 60)
2005-
timeInDay = timeInDay.truncatingRemainder(dividingBy: 60.0)
2006-
2007-
let second = Int(timeInDay)
1997+
let hour = secondsInDay / 3600
1998+
let minute = (secondsInDay % 3600) / 60
1999+
let second = secondsInDay % 60
20082000
let nanosecond = Int((localDate.timeIntervalSinceReferenceDate - dateOffsetInSeconds) * 1_000_000_000)
20092001

2002+
if components.containsOnlyTimeComponents {
2003+
var dcHour: Int?
2004+
var dcMinute: Int?
2005+
var dcSecond: Int?
2006+
var dcNano: Int?
2007+
if components.contains(.hour) { dcHour = hour }
2008+
if components.contains(.minute) { dcMinute = minute }
2009+
if components.contains(.second) { dcSecond = second }
2010+
if components.contains(.nanosecond) { dcNano = nanosecond }
2011+
return DateComponents(hour: dcHour, minute: dcMinute, second: dcSecond, nanosecond: dcNano)
2012+
}
2013+
20102014
let dayOfYear: Int
20112015
let weekday: Int
20122016
let weekOfMonth: Int
@@ -2018,6 +2022,7 @@ internal final class _CalendarGregorian: _CalendarProtocol, @unchecked Sendable
20182022
var month: Int
20192023
var day: Int
20202024
do {
2025+
let useJulianRef = useJulianReference(date)
20212026
let julianDay = try date.julianDay()
20222027
(year, month, day) = Self.yearMonthDayFromJulianDay(julianDay, useJulianRef: useJulianRef)
20232028
isLeapYear = gregorianYearIsLeap(year)

0 commit comments

Comments
 (0)