A blog exploring functional programming and Swift.

Better Performance Bonanza

Wednesday Jul 14, 2021

This past month we spent time improving the performance of several of our more popular libraries that are used in building applications in the Composable Architecture. Let’s recap those changes, explore some improvements that came in from the community, and celebrate the release of a brand new library.

Composable Architecture 0.21.0

First, we released a new version of the Composable Architecture, our library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind. It includes a number of performance improvements in the architecture’s runtime, which were covered in a dedicated episode. In particular we reduced the number of times scoping transformations and equality operators are invoked. This helps massively reduce the work performed for well-modularized applications.

While we made some great strides in this release, we did note that there was still more room for improvement, and a member of the community quickly came in to close the gap! Just a day later, Pat Brown submitted a pull request that fully minimized the number of equality checks performed in the view store. 😃

These changes have since been merged and are already available in a new version.

Case Paths 0.5.0

Next, we released a new version of Case Paths, our library that brings the power and ergonomics of key paths to enums. While key paths let you write code that abstracts over a field of a struct, case paths let you write code that abstracts over a particular case of an enum. Case paths are quite useful in their own right, but they also play an integral part in modularizing applications, especially those written in the Composable Architecture, which comes with many compositional operations that take key paths and case paths.

While a key path consists of a getter and setter, a case path consists of a pair of functions that can attempt to extract a value from, or embed a value in, a particular enum. For example, given an enum with a couple cases:

enum AppState {
  case loggedIn(LoggedInState)
  case loggedOut(LoggedOutState)
}

We can construct a case path for the loggedIn case that can embed or extract a value of LoggedInState:

CasePath(
  embed: AppState.loggedIn,
  extract: { appState in
    guard case let .loggedIn(state) = appState else { return nil }
    return state
  }
)

This is, unfortunately, a lot of boilerplate to write and maintain for what should be simple, especially when we consider that Swift’s key paths come with a very succinct syntax:

\String.count

And this is why we used reflection and a custom operator to make case paths just as ergonomic and concise:

/AppState.loggedIn

Unfortunately, reflection can be quite slow when compared to the work done in a more manual way, as a benchmark will show:

name       time        std         iterations
---------------------------------------------
Manual       41.000 ns ± 243.49 %     1000000
Reflection 8169.000 ns ±  55.03 %      106802

So we focused on closing the gap by utilizing the Swift runtime metadata, a change that shipped in a new version. We covered these improvements in last week’s episode.

With these changes, the happy path of extracting a value was over twice as fast as it was previously, while the path for failure was almost as fast as manual failure.

name                time        std        iterations
-----------------------------------------------------
Manual                39.000 ns ± 266.87 %    1000000
Reflection          3399.000 ns ±  85.54 %     354827
Manual: Failure       36.000 ns ± 608.33 %    1000000
Reflection: Failure   80.000 ns ± 588.42 %    1000000

But it gets even better! Again, shortly after release, a member of the community stepped in to make case path reflection almost as fast as the manual alternative. Rob Mayoff dove deeper into the Swift runtime and surfaced with two pull requests (#36, #37) that leverage runtime functionality that can extract a value from an enum case without any of the reflection overhead:

name               time       std        iterations
---------------------------------------------------
Success.Manual      35.000 ns ± 158.00 %    1000000
Success.Reflection 167.000 ns ±  70.60 %    1000000
Failure.Manual      37.000 ns ± 197.47 %    1000000
Failure.Reflection  82.000 ns ± 135.63 %    1000000

That’s over 50x faster than the original! 🤯

You can already see these improvements in CasePaths 0.5.0, released yesterday.

Identified Collections 0.1.0

Finally, on Monday we open sourced a brand new library called IdentifiedCollections. This library hosts IdentifiedArray, a type that shipped with the initial release of the Composable Architecture.

This data structure has now been extracted to its own package and rewritten to be more performant and correct. Check out the announcement for more details!

Try them out today!

If you’re building a Composable Architecture application, upgrade to version 0.21.0 today to see all of these improvements. Or even if you don’t use the Composable Architecture, you may find CasePaths 0.5.0 and IdentifiedCollections 0.1.0 useful on their own.


Subscribe to Point-Free

👋 Hey there! If you got this far, then you must have enjoyed this post. You may want to also check out Point-Free, a video series on functional programming and Swift.