Skip to content
This repository has been archived by the owner on Sep 19, 2018. It is now read-only.

JSONParser

mdmathias edited this page Sep 15, 2016 · 7 revisions

Why does Freddy include its own JSON parser?

Originally, Freddy did not parse JSON - it converted Data to the JSON enum by running the data through JSONSerialization and then recursively walking through the returned Any, converting each object to the corresponding JSON case. However, in real world use, we noticed a severe performance issue with large JSON documents - multiple seconds (!) to parse a JSON document of about 1.5 MiB on an iPhone 5.

Profiling led to the conclusion that the problem was not JSONSerialization itself (which is quite fast), but with the recursive walk over the returned Any, switching over the types to convert to JSON. To remedy this, Freddy now includes a pure Swift JSON parser which emits the JSON enum directly. This JSON parser is not as fast as JSONSerialization (see below), but it is close enough for our work.

Benchmarks

All of the following tests can be run on the benchmarks branch of this repository. Tests were run on a 2013 MacBook Pro with a 2.7 GHz i7 using Xcode 7.2 (Swift 2.1). The test document is downloaded by a build hook; at the time of this writing, it was about 16 MiB.

Pure Parsing

Parser Time (lower is better)
JSONSerialization 0.25s
Freddy.JSONParser 0.75s

Notes: JSONSerialization emits an Any. Freddy.JSONParser emits a Freddy.JSON. Our parser is approximately 3x slower than NSJSONSerialization.

Parsing plus conversion to Freddy.JSON

Technique Time (lower is better)
JSONSerialization then recursive type-switching 6.29s
Freddy.JSONParser 0.75s

Notes: These tests convert Data to Freddy.JSON. The conversion process from JSONSerialization's Any to Freddy.JSON is extremely expensive - about 6 seconds for this data. This is approximately 8.4x slower than Freddy.JSONParser.

Full deserialization

Technique Time (lower is better)
JSONSerialization to Objective-C model classes 0.58s
Freddy.JSONParser to Swift structs 2.08s

Notes: This comparison is not entirely fair, as the Objective-C model class deserialization is deficient (in comparison to the Freddy style): it does not return any error information if the JSON data doesn't match what the code expects, which can lead to incorrect types assigned to properties or application crashes (potentially both). We left this benchmark in because this is how much Objective-C JSON parsing code has been written, even though it's technically incorrect. We are within a factor of 4 with full Swift type-checking and error handling.

Optimization Suggestions

If maximum JSON performance (with Swift type safety) is your goal, the best advice is to follow standard Swift optimization tips. An optimization tip of particular note is that generic functions are not specialized across module boundaries, so you can achieve faster code by avoiding some of Freddy's generic functions. For example, this:

struct Foo {
    var bar: Int
    var baz: String
}

extension Foo: JSONDecodable {
    init(json: Freddy.JSON) throws {
        bar = try json.decode(at: "bar")
        baz = try json.decode(at: "baz")
    }
}

makes use of Freddy's generic decode method. You can get a slight performance increase by using the non-generic alternatives to decode:

struct Foo {
    var bar: Int
    var baz: String
}

extension Foo: JSONDecodable {
    init(json: Freddy.JSON) throws {
        bar = try json.getInt(at: "bar")
        baz = try json.getString(at: "baz")
    }
}

This should not be necessary in most cases, as the performance difference is usually small.