How expensive is DateFormatter
source link: https://sarunw.com/posts/how-expensive-is-dateformatter/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
How expensive is DateFormatter
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.
Learn everything you need to know about Sign in with Apple to be able to integrate it in your existing app or a new one.
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)date(from: String)
44.100
date(from: String)
with reuse DateFormatter
1.870
string(from: Date)
90.800
string(from: Date)
with reuse DateFormatter
30.400
The result surprised me. Since initializing of DateFormatter
is 1.37
milliseconds and calling of date(from: String)
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 date(from: String)
.
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
)
date(from: String)
1.870
string(from: Date)
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
.
To know for sure, we might need to dive down with more sophisticated tools like Instrument, but I won't do it in this article.
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) Setcalendar
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)date(from: String)
44.100
date(from: String)
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.
Operations Time(milliseconds) SetOperation without reuse of
DateFormatter
-44.1
(date(from: String)
)
calendar
119.900
Set locale
3.100
Set timezone
84.900
Set dateFormat
11.000
Set dateStyle
and timeStyle
40.200
This is quite weird since DateFormatter
should have a default value for all of its property. Setting them before calling string(from: Date)
shouldn't trigger any configuration (as I assume it will calculate lazily). Again, we need to dive down with a more sophisticated tool to understand this, but I won't do it in this article.
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 changetimeZone
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
:
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.
dateFormat
is the only property that cheap in changing.
Learn everything you need to know about Sign in with Apple to be able to integrate it in your existing app or a new one.
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
date(from: String)
is cheap. - Subsequence use of
string(from: Date)
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)
1.870
Use of string(from: Date)
30.400
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.
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.
How 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
TThere might be several reasons that cause this error. I will share one solution that fixes the one that happened to me the most.
Get new posts weekly
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 — entirely for free.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK