Speed up your DateFormatter
Maybe you have heard of the slow performance of DateFormatter when it's being initialized. But how slow is it really and what other calls may affect the performance of your app when working with date formats?
I came across that same question. So I decided to make a little experiment. Fortunately, Xcode has everything we need to measure that kind of metric. In your unit tests there is an option to measure the time of a code block:
self.measure {
// code block you want to measure
}
With this piece of code, it is fairly simple to run tests measuring the time for multiple kinds of operations. To get numbers that better compare I decided to run each target operation 10.000 times. The more samples you have the better the statistical results get.
One example test case looks like this:
func testPerformanceSetFormat() {
let dateFormatter = DateFormatter()
let dateString = "30.01.2020 19:35"
self.measure {
for _ in (0..<numberOfIterations) {
dateFormatter.dateFormat = "dd.MM.yyyy hh:mm"
let _ = dateFormatter.date(from: dateString)
}
}
}
In total, I came up with 12 test cases:
- timeInterval:
Date(timeIntervalSince1970: 597270773.0)
as a reference how long it takes to initialize aDate
with aTimeInterval
. This approach uses no DateFormatter at all. - DateFormatter (new): Create a new DateFormatter every time
- DateFormatter (reuse): Reuse one instance of a DateFormatter without any other changes. All other cases from here will reuse an instance of DateFormatter.
- dateFormat (set), calendar (set), timezone (set): Set the corresponding value every time to the same value.
- dateFormat (change), calendar (change), timezone (change): Changes the corresponding value to another value than before.
- calendar (once), timezone (once): Set the corresponding value only once before the measurement starts.
- cal + timezone (once): Set both values once before the measurement loop.
For the cases where a property is actually changed to another value than it had before, I changed the loop a bit:
func testPerformanceAlternatingdTimezone() {
let dateFormatter = DateFormatter()
let dateString = "30.01.2020 19:35"
let timezone1 = TimeZone(secondsFromGMT: 0)
let timezone2 = TimeZone(secondsFromGMT: 5)
self.measure {
for _ in (0..<numberOfIterations/2) {
dateFormatter.timeZone = timezone1
let _ = dateFormatter.date(from: dateString)
dateFormatter.timeZone = timezone2
let _ = dateFormatter.date(from: dateString)
}
}
}
The number of iterations is cut to half because there are twice as many operations inside the loop.
Additional to the 10.000 iterations per test case, Xcode runs all tests with a measure
block 10 times to calculate a mean value. This is necessary because it isn’t predictable how this single task will be scheduled inside the CPU, which results in varying run times. You can see this when you click on the grey diamond near the measure
block. In the pop-up, you can see how the 10 runs took different lengths of time.
I ran the complete test suite 3 times to be able to calculate my own mean values. So remember, the following numbers represent the seconds use for 10.000 operations for the given test case.
As expected, creating a Date
from a TimeInterval
is by far the fastest approach. But the surprise is that changing the calender
property costs much more time than instantiating a new DateFormatter. And it is nearly 5 times slower than setting it once. Also changing the format seems to be an expensive task, whereas changing the timezone causes no significant difference compared to not changing it. It also seems to be a good idea to set calendar
and timezone
at all instead of going with the default values of DateFormatter.
Conclusion
Based on these results when working with DateFormatter
it seems to be most efficient to have reusable instances for differently configured formatters. This was a date formatter has to be configured only once and can be used again and again with only the minimum amount of computation time. But if you have the chance to use timestamps instead of formatted date strings, you should use it.