Measuring the performance of mergeable libraries in iOS apps

January 2, 2024

I was curious about what the use of mergeable libraries meant for a typical app that may have a lot of library dependencies, so I devised a test which would measure the performance of merged frameworks versus old-school dynamic frameworks.

To read more about how mergeable libraries work, read this post.

The results

Let’s start with the results. These were taken on an iPhone 14 Pro using Instruments.

Graph. X axis shows number of frameworks from 0–100. Y axis shows time to UIKit initialization. First series: “individual frameworks”. The line goes from around 40 ms at zero frameworks, to about 60 ms at one, and rising linearly to just over 600 ms with 100. Second series: “merged frameworks”. The line hovers around 50 ms, and stays flat and rises slightly to just under 100 ms at 100. Third series: “merged frameworks with intermediate non-merged framework”. The line remains about as flat as the second series, but at a slightly higher plateau around 80 ms. The line rises steadily at 70 to reach about 180 ms at 100. Fourth series: static libraries. The line stays flat at around 30–50 ms.

And here is a close-up of the region around the origin:

Close-up of the previous graph around the origin

As you can see, the difference is dramatic. Virtually all of the O(n) cost of dynamic library loading is gone, even when an intermediate merged framework is introduced. It’s almost like the dynamic libraries have been turned into static ones, and that is not far from the truth.

The test

For the test, I programmatically created 100 small frameworks containing similar code but with unique symbols. In each framework, I exported

  • 100 trivial functions in C
  • 50 Objective-C classes, implemented in Swift1, each with unique selectors, for a total of 500 selectors in the library. Each library reused the same selectors.2 and contained trivial implementations.

You can download the test project with this configuration here.

For the “Individual frameworks” test, I linked and embedded anywhere from zero to 100 frameworks in a barebones app, and wrote code to invoke all of those functions and class methods.

I used the App Launch Instrument to profile the app, and measured the time between process creation and the UIKit Initialization signpost.

Screencap showing interval between process creation and UIKit initialization

For the “Merged frameworks” test, I repeated the above process but with merging enabled.

For the “Merged frameworks with intermediate non-merged framework” test, I created an intermediate, non-mergeable framework which merged the remaining framework. The app then linked that intermediate framework only.

Key observations

Plain old dynamic frameworks have linear loading cost. Loading each framework happens in a single-threaded, linear fashion. Because my frameworks are trivial, the predominant cost of loading a framework appears to be just opening the file, which in my test cases accounted for about 2.9 ms per framework. No doubt loading cost will rise as framework complexity rises.

Mergeable libraries are fast. Really fast. Mergeable libraries work by turning dynamic loading into static linking, which eliminates file-opening, symbol lookups, and fixups. Mergeable libraries take constant time to run regardless of how many frameworks are merged, because no load-time activity takes place.

Intermediate merged libraries are really fast too. Much to my surprise, using an intermediate, non-merged framework to merge the rest of the libraries resulted in a similar flat curve as merged libraries, just a bit slower. My guess is that optimization and consolidation within the merged library (such as Objective-C selector uniquing) can be done across libraries to make load-time faster, but I’m not sure.

There is an inflection point at around 85 frameworks. Curiously, there is a slight uptick in load time for merged libraries at around 85 frameworks. I’m not sure why, but I suspect there is some number of frameworks that remains too high to be bundled into an app even with mergeable libraries, although that number is much higher than it used to be.

Your results will no doubt differ from mine, depending on library complexity. But my key takeaway is that it is now possible to freely use dynamic libraries in your app without necessarily having to pay the penalty for doing so at load time. If you’re a big fan of dynamic library semantics (you should be) this should be exciting news to you.

  1. To learn more about how to create Objective-C classes that are implemented in Swift, check out this post

  2. I deliberately chose to include Objective-C classes and selectors because they benefit greatly from consolidation at build time.