A fast timestamp parser in Swift
10 July 2023
I wrote a timestamp parser in Swift. It's called Parse3339.
It's well known that DateFormatter
, the main timestamp formatter and parser Apple ships in Foundation, is not particularly fast. It's flexible and it's correct, but it takes its time. The newer ISO8601DateFormatter
has similar performance.
I haven't much worried about that in the recent years. A while ago I had a problem with slow parsing but that time a caching layer solved it, as the data had a ton of identical dates. Last week I saw date parsers rear their heads in an Instruments trace again, and this time the data wasn't amenable to caching. It was happening on a background queue and it was only a few hundred milliseconds. But once you start optimizing, it's hard to let go.
In the past a common solution was using strptime(3)
. This time I wanted to see how far I could get with Swift. I've usually used parser combinators for parsing, but these time stamps are so regular a simpler approach felt sufficient.
Time formats have two relevant standards: ISO 8601 and RFC 3339. RFC 3339 is mostly a subset of ISO 8601, but even it allows for all kinds of irrelevant silliness. Realistically the only format I care about is a full timestamp with a T
between date and time and possibly a Z
for time zone. When I don't support any other variations, the resulting parser is really small, straightforward and easy to modify if your needs are different.
It's also very fast. The parser itself easily breezes through hundreds of thousands timestamps per second on my Mac. However, in most cases, you need something other than a struct with a bunch of numbers. Things slowed down once I started creating DateComponents
and converting those to Date
s with a Calendar
. The code was still a few times faster than the platform parsers, but it was clearly leaving performance on the table. It was also using a lot of memory even though the parser itself was completely running on stack, allocating nothing. Benchmarking revealed that my code was using a similar amount of memory as DateFormatter
, whereas ISO8601DateFormatter
had a lighter footprint.
So back to the old Unix functions. Swift's C bridging is first-class; struct tm
and timegm(3)
are right there. After changing the code to use those, the whole String
to Date
conversion runs completely without heap allocations and it's around 6–7 times faster than when doing the detour via DateComponents
.
The final result, according to Benchmark, is maybe around 15x the speed of the Foundation parsers. I'm pretty happy with it. It's available as a Swift package, there's documentation, and if you need something different or don't feel like using a package, all the parsing code is in just one file you can copy over to your project.