`static let` vs computed properties, and binary size

I’m the maintainer of a library that contains 7000+ static let properties. The binary size of the package is quite large, which is understandable. but what i don’t quite get is why when i switch to make them computed properties, the binary size of the package drops by ~50%.

I expected the change to only affect the memory usage or performance, but not the binary size.

Does someone have an idea of what is happening under the hood?

1 Like

static let properties store their values as static data in the binary, whereas computed properties store how to calculate their values at runtime in the binary.

Due to many Swift optimizations (and optimization boundaries), performance could vary using one technique over the other. You should benchmark/profile major changes like this to make sure it is acceptable (in terms of binary size, runtime performance, usability, maintainability, etc). You could use package traits to support both at the same time allowing the developer to choose, but that would require a minimum of Swift 6.1.


Stripping the binary of symbols makes a huge difference in the binary size if you weren't doing it before (not mentioned in your post).

Regardless, I built your library on my 2019 Intel iMac from the stable and feature/binary-size branches. I see a ~12.6% drop when built in release mode and stripped. No idea what that would translate to on ARM as I don't have one available.

However, I do see a 42.1% binary size drop when building for debug (stripped and unstripped).

I usually only consider release numbers. A 12% drop in binary size is small, but is it worth it for your project (considering runtime performance metrics)?

2 Likes

Thanks for the reply and your investigation. Is there a resource for stripping symbols from a package? I would like to know the pros and cons, and how to do it.

Stripping is done, at least on Linux (I think it also works on macOS via Xcode command line tools), using the strip command from GNU.

You can also build a package excluding the symbols (remember to swift package clean before executing) using the -Xlinker -s flags (like swift build -c release -Xlinker -s). After doing some research, it looks like stripping the binary exhibits different behavior based on the OS and linkers (I daily drive Linux, so the linker aggressively strips symbols; on macOS it looks to be less aggressive). I used the flag approach in my previous post since your project doesn't have an executable.

Since the values have to be saved in the binary in both approaches, my intuition is computed property approach would have (slightly) larger size than let property approach. How come the actual result is opposite?

This is definitely an area where the compiler can do better (and hopefully work on compile-time evaluation and other stuff needed by embedded will improve the situation).

If you compare the two, with a simple example the version that uses static let stored properties is bigger because it actually contains both versions: it has the stored constant data (which also requires two new accessors to be generated, adding even more code) as well as the version inlined into the getter (because for this simple example, a struct with two fields, it's more efficient to just return those two values directly).

The version with the computed properties sheds all that and just has the computed getter.

The size/shape of the structs involved may/will change things depending on how much the optimizer chooses to inline/outline, but the lack of predictability here is one of my constant frustrations.

11 Likes