Benchmarking Codable
JSONDecoder vs. JSONSerialization... FIGHT!
Swift Codable can automatically synthesize initializers that decode models from JSON. But how does this generated code compare to what it replaces?
To find out, let’s benchmark the performance of
JSONDecoder
against equivalent hand-written code that uses
JSONSerialization
instead.
#Defining a Sample Model
In order to establish a baseline, we’ll create a model that reasonably approximates something that you’d expect to find in a typical app.
For this example,
we’ll use the following Airport
model:
struct Airport: Codable {
let name: String
let iata: String
let icao: String
let coordinates: [Double]
struct Runway: Codable {
enum Surface: String, Codable {
case rigid, flexible, gravel, sealed, unpaved, other
}
let direction: String
let distance: Int
let surface: Surface
}
let runways: [Runway]
}
The Airport
structure has String
properties
for its name,
along with
three-letter IATA
and
four-letter ICAO
airport codes.
The Runway
type specifies a direction and distance,
as well as a surface defined by a nested Surface
enumeration.
A set of coordinates are stored in a [Double]
array,
though we could also define a custom type for that, too.
By conforming to Codable
in its declaration
and having stored properties with types that conform to Codable
,
Swift automatically synthesizes the implementation for the
init(from:)
initializer required by the
Decodable
protocol
(the same goes for the
Encodable
protocol and its
encode(to:)
method).
Airport
may not check all the boxes in terms of Codable
functionality,
but it has sufficient complexity to extrapolate from our findings.
And for whatever deficiencies this model has,
it more than makes up for it by having real-world data to test with.
As the saying goes, “Quantity has a quality all its own”.
By scraping Wikipedia’s “List of Airports” article, we were able to create a list of 7361 airports, from which we generated JSON files with 1, 10, 100, 1000, and 10000 objects (some records were duplicated to fill in the gaps for that last one).
{
"name": "Portland International Airport",
"iata": "PDX",
"icao": "KPDX",
"coordinates": [-122.5975, 45.5886111111111],
"runways": [
{
"distance": 1829,
"direction": "3/21",
"surface": "flexible"
}
// ...
]
}
Taking a look at the relative sizes of these data sets:
Count | Size | gzip Compressed Size |
---|---|---|
1 | 271 Bytes | 193 Bytes |
10 | 2.8 KB | 703 Bytes |
100 | 33.0 KB | 4.7 KB |
1000 | 328.0 KB | 44.3 KB |
10000 | 3.2 MB | 477.4 KB |
Most apps don’t process more than tens of thousands of records at once, so our benchmark should be fairly representative as far as sample sizes go.
#Manually Implementing a JSON Initializer
The conventional way to decode models from JSON without Codable
is to implement an initializer that takes a [String: Any]
type.
A hand-rolled implementation of this approach for Airport
weighs in at ~30 lines of code
(maybe 5 to 10 minutes to write from scratch):
extension Airport {
public init(json: [String: Any]) {
guard let name = json["name"] as? String,
let iata = json["iata"] as? String,
let icao = json["icao"] as? String,
let coordinates = json["coordinates"] as? [Double],
let runways = json["runways"] as? [[String: Any]]
else {
fatal Error("Cannot initialize Airport from JSON")
}
self.name = name
self.iata = iata
self.icao = icao
self.coordinates = coordinates
self.runways = runways.map { Runway(json: $0) }
}
}
extension Airport.Runway {
public init(json: [String: Any]) {
guard let direction = json["direction"] as? String,
let distance = json["distance"] as? Int,
let surface Raw Value = json["surface"] as? String,
let surface = Surface(raw Value: surface Raw Value)
else {
fatal Error("Cannot initialize Runway from JSON")
}
self.direction = direction
self.distance = distance
self.surface = surface
}
}
With effective use of guard
statements and the map(_:)
method,
this isn’t a particularly unappetizing chunk of boilerplate.
But it’s hard to compete with
the zero additional lines of code required of Codable.
#Creating Performance Tests
We use Xcode’s built-in testing framework, XCTest to measure the performance of each implementation.
In the setup for both tests (not shown here),
a count is specified and the corresponding data set is loaded.
Each test then decodes that data within a closure passed to the
measure(_:)
class Performance Tests: XCTest Case {
var data: Data
var count: Int
func test Performance Codable() {
self.measure {
let decoder = JSONDecoder()
let airports = try! decoder.decode([Airport].self, from: data)
XCTAssert Equal(airports.count, count)
}
}
func test Performance JSONSerialization() {
self.measure {
let json = try! JSONSerialization.json Object(with: data, options: []) as! [[String: Any]]
let airports = json.map{ Airport(json: $0) }
XCTAssert Equal(airports.count, count)
}
}
}
#Benchmarking Execution Time
You can download the Xcode project used to produce these results on GitHub.
Because JSONDecoder
uses JSONSerialization
under the hood,
we should expect the performance characteristics to be similar.
And indeed that’s what we see here:
Wall Clock Time (Smaller is Better)
Count | JSONSerialization | Codable | Δ |
---|---|---|---|
1 | 0.5 ms | 0.8 ms | +0.3 ms |
10 | 1 ms | 4 ms | +3 ms |
100 | 3 ms | 8 ms | +5 ms |
1000 | 30 ms | 51 ms | +21 ms |
10000 | 382 ms | 603 ms | +221 ms |
Swift 4.1, Xcode 9.3 (9E145), iPhone X Simulator
2017 MacBook Pro, 2.9 GHz Intel Core i7, 16 GB 2133 MHz LPDDR3
On average,
Codable with JSONDecoder
is about half as fast as
the equivalent implementation with JSONSerialization
.
But does this mean that we shouldn’t use Codable? Probably not.
A 2x speedup factor may seem significant, but measured in absolute time difference, the savings are unlikely to be appreciable under most circumstances — and besides, performance is only one consideration in making a successful app.
If you have a codebase that uses JSONSerialization
—
whether directly or through a third-party framework —
you might add benchmarks to see how Codable performs
against your existing implementations.
If performance is acceptable,
you could then proceed to build new functionality with Codable
before eventually transitioning existing code over.
Ultimately, every project is different, and it’s up to you to determine what’s right for you.
Codable isn’t a silver bullet,
but it’s good enough that we should consider it to be our new default.
Unless you have a specific reason to use JSONSerialization
,
Codable is an excellent choice for working with data representations.