Just how expensive is creating an NSDateFormatter?

One of the many mantras that is drilled into iOS devs is that you shouldn’t recreate expensive, complex objects like formatters every time you need them. Instead, keep them around in a static or instance variable and use them as needed. To test this, I created a playground and put this file in the Sources folder:

import Foundation
import QuartzCore
public func testWithMultipleInstantiation() -> CFTimeInterval {
var dateStrings: [String] = []
dateStrings.reserveCapacity(100000)
let start = CACurrentMediaTime()
for _ in 0..<100000 {
let df = NSDateFormatter()
df.dateStyle = .MediumStyle
df.timeStyle = .MediumStyle
dateStrings.append(df.stringFromDate(NSDate()))
}
let end = CACurrentMediaTime()
return end - start
}
public func testWithSingleInstantiation() -> CFTimeInterval {
var dateStrings: [String] = []
dateStrings.reserveCapacity(100000)
let start = CACurrentMediaTime()
let df = NSDateFormatter()
df.dateStyle = .MediumStyle
df.timeStyle = .MediumStyle
for _ in 0..<100000 {
dateStrings.append(df.stringFromDate(NSDate()))
}
let end = CACurrentMediaTime()
return end - start
}
public func testWithStaticMethod() -> CFTimeInterval {
var dateStrings: [String] = []
dateStrings.reserveCapacity(100000)
let start = CACurrentMediaTime()
for _ in 0..<100000 {
dateStrings.append(NSDateFormatter.localizedStringFromDate(NSDate(), dateStyle: .MediumStyle, timeStyle: .MediumStyle))
}
let end = CACurrentMediaTime()
return end - start
}
view raw FormatterTest.swift hosted with ❤ by GitHub

I didn’t put the code directly into the playground because doing ANYTHING in a playground 100k times is going to take a very long time and not be very representative of real-world conditions.

Here’s the playground code, which simply invokes the tests and records the results:

import Foundation
let timeForMultiple = testWithMultipleInstantiation()
let timeForSingle = testWithSingleInstantiation()
let timeForStatic = testWithStaticMethod()
print("multiple:", timeForMultiple) //multiple: 7.96959602891002
print("single: ", timeForSingle) //single: 0.888885863008909
print("static: ", timeForStatic) //static: 7.89041009696666
view raw Playground.swift hosted with ❤ by GitHub

The results were unequivocal: creating an NSDateFormatter each time you need to use it is roughly 9x slower than creating it once and using it repeatedly. That said, this is only likely to be an issue for tasks where there is a lot of date formatting going on, such as in a table view with dates in every cell. If you have a situation where you only need to format a single date infrequently (today’s date in a page header, for example), then you shouldn’t worry about hanging on to the date formatter; even though it’s heavy, it only takes a tiny fraction of a second to instantiate one.

CONVENTIONAL WISDOM: CONFIRMED

Addendum: At the request of a coworker (@rexeisen), I added in a test of the static NSDateFormatter.
localizedStringFromDate(_:dateStyle:timeStyle:)
test. As you can see, the results are no better than creating an instance each time. That said, it would be more convenient to use for 1-off formatting tasks, where keeping the formatter around is unnecessary.

Addendum II (2016-01-15): Further testing has revealed that changing the timeStyle and dateStyle of an NSDateFormatter is tremendously expensive. Even more so than just creating a new formatter for each use! Across several trials, performance using a single NSDateFormatter that is re-parameterized on each use was 10% slower than creating a new formatter each time and a full 10x slower than using a single, pre-configured formatter. The take away here is, create a formatter for each repeating case, don’t try to make a single shared formatter do all the work.