humancode.us

All about libraries

January 3, 2024

After writing the post about mergeable libraries, I thought I should write a post about how libraries work in general, and capture some recommendations on how to use them.

Let’s start at the beginning, with what happens when a source file is compiled.

A compiler creates object files from source files

A compiler takes a translation unit (a source file with any imported headers), and produces a compiled object file, containing the code and data defined in the translation unit.

Exported symbols

An object file may export symbols. Exported symbols are strings that identify data structures within the object file, which may be function implementations, constants, initialized data, whatever.

// sound.c

static int tweetCount = 0;

extern void tweet(void) {
    tweetCount++;
}

The file above compiles into an object file sounds.o which exports the symbol _tweet for the implementation of the tweet() function1 2.

Some symbols are purely internal to the translation unit. tweetCount, for example, is not exported.

In addition to open/public (exported) and private/fileprivate (not exported), Swift symbols have an access control option, package, which makes the symbol visible only within a Swift package. As far as libraries are concerned, package symbols are exported.

  1. By convention, C functions are exported with a leading underscore. 

  2. By default, C symbols are exported unless marked static. You can flip this behavior by setting GCC_SYMBOLS_PRIVATE_EXTERN = YES, and using visibility attributes to export symbols. 

Read more…

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.

Read more…

All about mergeable libraries

January 2, 2024

Mergeable Libraries is a technology introduced in WWDC23 that allows you to “absorb” and reexport dynamic libraries into a higher-level dynamic library, or directly into your app’s binary.

To see the performance implications of using mergeable libraries in your app, see this post.

How mergeable libraries work

The mergeable libraries feature allows you to copy the contents of dependent1 dynamic libraries one level up while maintaining the illusion in code that the original dependent libraries still existed where they were, when using API such as dlopen and Bundle.

Because the contents of dependent libraries are copied into the binary that links them, unresolved symbols in that binary can be statically resolved against the library’s contents which are now stored within the same binary. This is how mergeable libraries improve load times: it allows the linker to turn load-time resolution of symbols into link-time resolution, giving you dynamic-library semantics with near-static-library performance.

  1. I use the term “dependent libraries” to refer to a library that is linked by another binary, i.e. the child nodes in a link dependency tree. I know this term can be confusing, but this is the terminology Apple uses in their docs. 

Read more…

Welcome progress, no matter how small

December 22, 2023

Remember to welcome progress, no matter how small, no matter how late.

It’s easy for someone who’s been shouting warnings from the rooftops to be frustrated that people are only now beginning to listen to them, and then make such small and entirely-insufficient changes based on that “new” information.

But that is what progress looks like. People can’t pay attention to everything, because they have their entire lives to contend with. So when they start to pay attention to your message, it is vital that you don’t turn against them for doing so, but instead nurture their interest and direct their next steps.

Yes, you’ve seen it coming. You’ve been experiencing it. You know what needs to be done to fix the issue. But not everyone does. And when someone begins to show signs of acquiring a sliver of your insight, celebrate it.

That is what progress looks like.

Go small to go big

November 29, 2023

When I’m in the zone coding, especially if it’s a moderately complex program that I understand well, I get hyperfocused. It’s really hard for me to put the project down. The idea for the next building block, the next simplifying refactor, the next feature burn in my brain, and I need to get it out and into code. It is challenging when I have other important tasks to do, people to take care of, or if I have to eat or sleep.

The danger with obsession is of course to rathole—to push yourself beyond your optimal cognitive load, to hyper-optimize, to push a feature too far, too early. I spent a whole day this past weekend implementing a major refactor that ended up being entirely wrong-headed, and I had to git reset --hard and admit defeat. My current implementation is much like the original, just slightly more finessed.

One way I address this (which I forgot to do this past weekend) is to take a grand idea and apply it to something as small as possible. Want to rearchitect a whole class cluster? Try modifying one method in one class. Want to refactor 10 files? Do it with one. Want to rewrite a function that is too large from scratch? Extricate one if branch instead. Want to add a utility library? Start with one function in an empty file.

For me, it turns out that what drives my burning obsession is a constant desire for a taste of progress. Turns out that small progress is far easier to achieve than large progress, so breaking down a grand vision into its smallest possible manifestation allows me to make some progress quickly and satisfy my thirst, and also cheaply test whether the improvement was worth it in the end—and if not, the amount of work discarded would also be small.

Making the smallest possible change that implements your grand vision has a wonderful side effect of making each of your commits do only one small thing. The smallest implementation of your refactor could literally be “Rename foo to bar”, which would make a fine commit message: clear and descriptive. “Reparent Foo class cluster and protocols” on the other hand, will probably be a huge mess of a diff. It’s better to have a commit log of 20 tiny isolated changes than one big one.

So I’m committed to focusing on small progress from now on. When it’s time to step away for meals, to socialize, or to take a walk, I would be satisfied to see that I’ve pushed five small changes to the repo, representing five steps toward a grand plan—a plan that I can now revise with the hindsight of testing if those first five steps were going in the right direction. Because paradoxically, small steps let you make big changes faster.