How expensive is DateFormatter
Table of Contents
Part one in a series on DateFormatter performance. In the first part, we will answer how expensive is DateFormatter and which operation is costly.
If you are working on iOS for long enough, there is a chance that you might have known one performance tip about DateFormatter
. That is, creating it is an expensive operation.
Creating DateFormatter is an expensive operation.
Everyone around me seems to know this tip. I know it, even though I can't remember where I got this tip from, maybe one of WWDC sessions. Since then, I'm fully aware every time I use DateFormatter
.
What I don't know is how slow it is and which part that it is expensive. We are going to find out together in this article. I will use XCTest's measure
method (open func measure(_ block: () -> Void)
) as a tool to gauge the performance. It might not be the right tool, but it can give us a rough idea of how expensive each operation is.
The following is a boilerplate of how we are going to test
class DateFormatterTests: XCTestCase {
let numberOfIterations = 1000
func testHypothesis() {
self.measure {
for _ in (0..<numberOfIterations) {
// Our hypothesis
}
}
}
}
Experiment #1: How expensive is DateFormatter creation?
To test this, we try to compare the creation of DateFormatter
instance with others. My candidates are DateFormatter
, Date
, NSString
, and UIView
.
func testDateFormatterCreation() throws {
self.measure {
for _ in (0..<numberOfIterations) {
let df = DateFormatter()
}
}
}
func testDateCreation() throws {
self.measure {
for _ in (0..<numberOfIterations) {
let date = Date()
}
}
}
func testNSStringCreation() throws {
self.measure {
for _ in (0..<numberOfIterations) {
let foo = NSString(format: "foo")
}
}
}
func testViewCreation() throws {
self.measure {
for _ in (0..<numberOfIterations) {
let view = UIView()
}
}
}
Result:
Creation | Time(milliseconds) |
---|---|
Date |
0.834 |
DateFormatter |
1.370 |
NSString |
2.220 |
UIView |
4.880 |
As you can see, there is no significant time difference between DateFormatter
and other instances.
You can easily support sarunw.com by checking out this sponsor.
AI Grammar: Correct grammar, spell check, check punctuation, and parphrase.
Experiment #2: How expensive is DateFormatter when using
Since the initialization process is not a problem, we will try to call common methods that we always use with DateFormatter
, date(from: String)
and string(from: Date)
.
func testDateFormatterCreationAndPrint() throws {
self.measure {
for _ in (0..<numberOfIterations) {
let date = Date()
let df = DateFormatter()
let dateString = df.string(from: date)
}
}
}
func testDateFormatterCreationAndPrintWithCache() throws {
let df = DateFormatter()
self.measure {
for _ in (0..<numberOfIterations) {
let date = Date()
let dateString = df.string(from: date)
}
}
}
func testDateFormatterCreationAndParse() throws {
self.measure {
for _ in (0..<numberOfIterations) {
let df = DateFormatter()
let date = df.date(from: "30/01/2020")
}
}
}
func testDateFormatterCreationAndParseWithCache() throws {
let df = DateFormatter()
self.measure {
for _ in (0..<numberOfIterations) {
let date = df.date(from: "30/01/2020")
}
}
}
Result:
Creation | Time(milliseconds) |
---|---|
string(from: Date) |
44.100 |
string(from: Date) with reuse DateFormatter |
1.870 |
date(from: String) |
90.800 |
date(from: String) with reuse DateFormatter |
30.400 |
The result surprised me. Since initializing of DateFormatter
is 1.37
milliseconds and calling of string(from: Date)
with reuse DateFormatter
is 1.87
, I expected testDateFormatterCreationAndPrint
to be somewhere around 3.24
milliseconds (1.37
+ 1.87
). Turn out it took much more than that, ~14x more to be exact (44.1
).
I guess that DateFormatter
has some lazy implementation, which will be executing once it needs to do the actual calculation. Moving DateFormatter
out of the running loop significantly reduce execution time because we avoid expensive setup after each call of string(from: Date)
.
From the result, we can get a rough estimation of each operation.
Operations | Time(milliseconds) |
---|---|
DateFormatter set up |
42.230 ~ 60.400 (44.1 - 1.87 and 90.8 - 30.4 ) |
string(from: Date) |
1.870 |
date(from: String) |
30.400 |
Let's go back to our previous results and update them with our new information.
Updated Result:
Creation | Time(milliseconds) |
---|---|
Date |
0.834 |
DateFormatter |
1.370 to 42.230 ~ 60.400 |
NSString |
2.220 |
UIView |
4.880 |
It becomes more clear why Apple said DateFormatter
creation is expensive. It isn't visible at first because of the lazy implementation of DateFormatter
.
Experiment #3: How expensive is DateFormatter with customization
Now we know that there is some processing cost of the first initializing DateFormatter
. Let's see if changing property like calendar
, timezone
, locale
, dateFormat
, and dateStyle
would cause the same cost or not.
func testDateFormatterCreationAndSetCalendarAndPrint() throws {
self.measure {
for _ in (0..<numberOfIterations) {
let date = Date()
let df = DateFormatter()
df.calendar = Calendar(identifier: .buddhist)
let dateString = df.string(from: date)
}
}
}
func testDateFormatterCreationAndSetCalendarAndPrintWithCache() throws {
let df = DateFormatter()
self.measure {
for _ in (0..<numberOfIterations) {
let date = Date()
df.calendar = Calendar(identifier: .buddhist)
let dateString = df.string(from: date)
}
}
}
func testDateFormatterCreationAndSetTimezoneAndPrint() throws {
self.measure {
for _ in (0..<numberOfIterations) {
let date = Date()
let df = DateFormatter()
df.timeZone = TimeZone(secondsFromGMT: 60 * 60 * 7)
let dateString = df.string(from: date)
}
}
}
func testDateFormatterCreationAndSetTimezoneAndPrintWithCache() throws {
let df = DateFormatter()
self.measure {
for _ in (0..<numberOfIterations) {
let date = Date()
df.timeZone = TimeZone(secondsFromGMT: 60 * 60 * 7)
let dateString = df.string(from: date)
}
}
}
func testDateFormatterCreationAndSetLocaleAndPrint() throws {
self.measure {
for _ in (0..<numberOfIterations) {
let date = Date()
let df = DateFormatter()
df.locale = Locale(identifier: "th")
let dateString = df.string(from: date)
}
}
}
func testDateFormatterCreationAndSetLocaleAndPrintWithCache() throws {
let df = DateFormatter()
self.measure {
for _ in (0..<numberOfIterations) {
let date = Date()
df.locale = Locale(identifier: "th")
let dateString = df.string(from: date)
}
}
}
func testDateFormatterCreationAndSetDateFormatAndPrint() throws {
self.measure {
for _ in (0..<numberOfIterations) {
let date = Date()
let df = DateFormatter()
df.dateFormat = "dd"
let dateString = df.string(from: date)
}
}
}
func testDateFormatterCreationAndSetDateFormatAndPrintWithCache() throws {
let df = DateFormatter()
self.measure {
for _ in (0..<numberOfIterations) {
let date = Date()
df.dateFormat = "dd"
let dateString = df.string(from: date)
}
}
}
func testDateFormatterCreationAndSetDateStyleAndPrint() throws {
self.measure {
for _ in (0..<numberOfIterations) {
let date = Date()
let df = DateFormatter()
df.dateStyle = .long
df.timeStyle = .long
let dateString = df.string(from: date)
}
}
}
func testDateFormatterCreationAndSetDateStyleAndPrintWithCache() throws {
let df = DateFormatter()
self.measure {
for _ in (0..<numberOfIterations) {
let date = Date()
df.dateStyle = .long
df.timeStyle = .long
let dateString = df.string(from: date)
}
}
}
Results:
Operations | Time(milliseconds) |
---|---|
Set calendar |
164.000 |
Set calendar with reuse DateFormatter |
14.100 |
Set timeZone |
129.000 |
Set timeZone with reuse DateFormatter |
5.430 |
Set locale |
47.200 |
Set locale with reuse DateFormatter |
2.130 |
Set dateFormat |
55.100 |
Set dateFormat with reuse DateFormatter |
2.680 |
Set dateStyle and timeStyle |
84.300 |
Set dateStyle and timeStyle with reuse DateFormatter |
5.120 |
Reference:
Operations | Time(milliseconds) |
---|---|
string(from: Date) |
44.100 |
string(from: Date) with reuse DateFormatter |
1.870 |
We can tell that each property setting can take an extra amount of processing time from the result.
Estimation of extra processing time needed for each operation.
Operation without reuse of
DateFormatter
-90.8
(string(from: Date)
)
Operations | Time(milliseconds) |
---|---|
Set calendar |
119.900 |
Set locale |
3.100 |
Set timezone |
84.900 |
Set dateFormat |
11.000 |
Set dateStyle and timeStyle |
40.200 |
One thing to highlight here is that this execution time doesn't add up when we reuse DateFormatter
, which means there is some kind of checking that prevents reconfiguring DateFormatter
if the same value is set.
I suspect that there is something like this, which is the right thing to do.
var calendar: Calendar! {
didSet {
guard calendar != oldValue else {
return
}
// Expensive operation
}
}
Experiment #4: How expensive is changing DateFormatter properties
To get the cost of changing DateFormatter
property, we need to avoid setting the same value. I redesign our test to something like this.
We reduce the number of iterations by two (as we double the operations) and change value twice, so the next loop would gurantee that setting value will trigger the reconfiguration.
func testDateFormatterCreationAndSetTimezoneAndPrintBackAndForth() throws {
self.measure {
for _ in (0..<numberOfIterations/2) {
let date = Date()
let df = DateFormatter()
df.timeZone = TimeZone(secondsFromGMT: 60 * 60 * 7)
_ = df.string(from: date)
df.timeZone = TimeZone(secondsFromGMT: 60 * 60 * 6)
_ = df.string(from: date)
}
}
}
func testDateFormatterCreationAndSetTimezoneAndPrintWithCacheBackAndForth() throws {
let df = DateFormatter()
self.measure {
for _ in (0..<numberOfIterations/2) {
let date = Date()
df.timeZone = TimeZone(secondsFromGMT: 60 * 60 * 7)
_ = df.string(from: date)
df.timeZone = TimeZone(secondsFromGMT: 60 * 60 * 6)
_ = df.string(from: date)
}
}
}
func testDateFormatterCreationAndSetDateFormatAndPrintBackAndForth() throws {
self.measure {
for _ in (0..<numberOfIterations/2) {
let date = Date()
let df = DateFormatter()
df.dateFormat = "dd"
let dateString = df.string(from: date)
df.dateFormat = "dd MM"
let dateString2 = df.string(from: date)
}
}
}
func testDateFormatterCreationAndSetDateFormatAndPrintWithCacheBackAndForth() throws {
let df = DateFormatter()
self.measure {
for _ in (0..<numberOfIterations/2) {
let date = Date()
df.dateFormat = "dd"
let dateString = df.string(from: date)
df.dateFormat = "dd MM"
let dateString2 = df.string(from: date)
}
}
}
func testDateFormatterCreationAndSetCalendarAndPrintBackAndForth() throws {
self.measure {
for _ in (0..<numberOfIterations/2) {
let date = Date()
let df = DateFormatter()
df.calendar = Calendar(identifier: .buddhist)
let dateString = df.string(from: date)
df.calendar = Calendar(identifier: .gregorian)
let dateString2 = df.string(from: date)
}
}
}
func testDateFormatterCreationAndSetCalendarAndPrintWithCacheBackAndForth() throws {
let df = DateFormatter()
self.measure {
for _ in (0..<numberOfIterations/2) {
let date = Date()
df.calendar = Calendar(identifier: .buddhist)
let dateString = df.string(from: date)
df.calendar = Calendar(identifier: .gregorian)
let dateString2 = df.string(from: date)
}
}
}
func testDateFormatterCreationAndSetLocaleAndPrintBackAndForth() throws {
self.measure {
for _ in (0..<numberOfIterations/2) {
let date = Date()
let df = DateFormatter()
df.locale = Locale(identifier: "th_TH")
let dateString = df.string(from: date)
df.locale = Locale(identifier: "en_US")
let dateString2 = df.string(from: date)
}
}
}
func testDateFormatterCreationAndSetLocaleAndPrintWithCacheBackAndForth() throws {
let df = DateFormatter()
self.measure {
for _ in (0..<numberOfIterations/2) {
let date = Date()
df.locale = Locale(identifier: "th_TH")
let dateString = df.string(from: date)
df.locale = Locale(identifier: "en_US")
let dateString2 = df.string(from: date)
}
}
}
func testDateFormatterCreationAndSetDateStyleAndPrintBackAndForth() throws {
self.measure {
for _ in (0..<numberOfIterations/2) {
let date = Date()
let df = DateFormatter()
df.dateStyle = .long
df.timeStyle = .long
let dateString = df.string(from: date)
df.dateStyle = .short
df.timeStyle = .short
let dateString2 = df.string(from: date)
}
}
}
func testDateFormatterCreationAndSetDateStyleAndPrintWithCacheBackAndForth() throws {
let df = DateFormatter()
self.measure {
for _ in (0..<numberOfIterations/2) {
let date = Date()
df.dateStyle = .long
df.timeStyle = .long
let dateString = df.string(from: date)
df.dateStyle = .short
df.timeStyle = .short
let dateString2 = df.string(from: date)
}
}
}
Results:
Operations | Time(milliseconds) |
---|---|
Set and change timeZone |
105.000 |
Set and change timeZone with reuse DateFormatter |
81.600 |
Set and change dateFormat |
32.500 |
Set and change dateFormat with reuse DateFormatter |
2.440 |
Set and change calendar |
138.000 |
Set and change calendar with reuse DateFormatter |
101.000 |
Set and change locale |
48.200 |
Set and change locale with reuse DateFormatter |
48.500 |
Set and change dateStyle |
70.900 |
Set and change dateStyle with reuse DateFormatter |
69.300 |
Reference of setting property without reuse of DateFormatter
:
Operations | Time(milliseconds) |
---|---|
Set timeZone |
129.000 |
Set dateFormat |
55.100 |
Set calendar |
164.000 |
Set locale |
47.200 |
Set dateStyle |
84.300 |
As you can see, changing some properties back and forth can cost us the same amount of time no matter we reuse DateFormatter
or not.
Conclusion
This is quite a long article full of stats and numbers. The key take away from all these stats are:
DateFormatter
is expensive to create.DateFormatter
is expensive to change.- Subsequence use of
string(from: Date)
is cheap. - Subsequence use of
date(from: String)
isn't cheap.
Rough estimation of operations:
Operations | Time(milliseconds) |
---|---|
DateFormatter creation |
42.230 ~ 60.400 |
Change timeZone |
81.600 |
Change dateFormat |
2.440 |
Change calendar |
101.000 |
Change locale |
48.500 |
Change dateStyle |
69.300 |
Use of date(from: String) |
30.400 |
Use of string(from: Date) |
1.870 |
When I say expensive, it doesn't mean your app would cause a memory warning just by using DateFormatter
. The hardware of new devices is far superior to the past. You might not get a noticeable lag even when you don't reuse DateFormatter
, but I think it is good to save as much as you can even you have plenty of resources to do so.
In the next article, we will use this knowledge and see how we should use DateFormatter
in our code.
You can easily support sarunw.com by checking out this sponsor.
AI Grammar: Correct grammar, spell check, check punctuation, and parphrase.
Related Resources
Read more article about DateFormatter, Optimization, Swift, or see all available topic
Enjoy the read?
If you enjoy this article, you can subscribe to the weekly newsletter.
Every Friday, you'll get a quick recap of all articles and tips posted on this site. No strings attached. Unsubscribe anytime.
Feel free to follow me on Twitter and ask your questions related to this post. Thanks for reading and see you next time.
If you enjoy my writing, please check out my Patreon https://www.patreon.com/sarunw and become my supporter. Sharing the article is also greatly appreciated.
Become a patron Buy me a coffee Tweet ShareHow to get the first N elements of array in Swift
Learn a few ways to do it and things you should know when using them.
How to fix "Build input file cannot be found" error in Xcode
There might be several reasons that cause this error. I will share one solution that fixes the one that happened to me the most.