When working on iOS performance, I often heard developer concerns about allocations and usage of pointers for such fundamental and frequently used types as NSNumber
and NSString
. Even leaving allocations aside, going through pointer indirection on every access would most certainly not help branch predictor and cache hits. But let’s take a look at the following snippet of code:
All it does is creates and outputs addresses of a series of numbers 1, 11, 111, … But the output reveals something very interesting:
memory address of the
NSNumber
does not change between iterationsthe
NSNumber
pointers are odd, which looks strange since memory is accessed on the byte boundary + alignment, so it should always be even
So what does this mean? It means that Apple engineers were also concerned about performance implications of unnecessary allocations and memory indirection and have used the hero of this article - tagged pointers. It’s somewhat similar to small string optimization that we’ve already discussed but instead of using other fields, we take advantage of the unused pointer bits
This way, depending on architecture, we are able to embed the value directly into the pointer, avoid any allocations and access the value without any indirection. Lack of allocations is the reason for observation #1, as all NSNumber*
end up on the stack instead of heap.
And this technique is not limited just to numbers.
produces
that shows how NSTaggedPointerString
is used to squeeze in 11 ones directly into the pointer before switching to a regular heap allocated __NSCFString
.
I intentionally avoid going into the tagged pointer implementation details since they are subject to change and should not be relied upon. In fact, that’s the reason why
that looks like a great candidate for a tagged pointer, is backed by __NSCFConstantString
, since it has to preserve ABI compatibility.
This simple yet powerful technique can enable significant performance boost and memory savings.