Ad

Our DNA is written in Swift
Jump

Caching Caches

While doing some performance tuning on the iCatalog.framework I stumbled upon a method of about 7 statements where a single line was responsible for more than a third of all CPU time.

This basically was only getting the path to the app’s Library/Caches folder. By itself this statement looks very innocent and I had it in about a dozen places all around the app. But it turns out that if you calling it hundreds or thousands of times then the time it takes to search for the Caches (and Documents) path sums up enormously.

Interestingly it does not seem like Apple implemented any caching for them so they seem to use around the same time all the time. But these values are prime candidates for caching because they won’t change while your app is running. Also the objc function call to get a cached version of the paths is several orders of magnitude faster than determining it in the first place.

This is what I saw in Instruments:

Granted, in most apps you would probably never notice a difference, but if you work with the documents or caches folders frequently in loops and/or many places then this is for you.

I added two category methods to my DTFoundation project to add caching.

@implementation NSString (DTPaths)
 
+ (NSString *)cachesPath
{
	static dispatch_once_t onceToken;
	static NSString *cachedPath;
 
	dispatch_once(&onceToken, ^{
		cachedPath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];	
	});
 
	return cachedPath;
}
 
+ (NSString *)documentsPath
{
	static dispatch_once_t onceToken;
	static NSString *cachedPath;
 
	dispatch_once(&onceToken, ^{
		cachedPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];	
	});
 
	return cachedPath;
}
@end

I introduced this pattern using a “onceToken” previously and it proves to be a boon. So you should learn it by heart. You create a static dispatch once token as well as a static pointer to something that you want to cache. Then you wrap the creation of the item in a dispatch_once. Et voila!

Before GCD we would have done this sort of caching with a if (!cachedPath) but the GCD-way has the added benefit of being thread-safe.

As a general rule of thumb you should cache everything that is expensive to get (calculation or via network) and does not change much. If it is just a single pointer, like an NSString * the above approach works marvelously. If you want to see how this can be combined with the use of NSCache for multiple values, check out …

OMG, I was about to write “checkout the DTFontDescriptor class in DTCoreText” as a shining example of how to initialize a font cache. But here’s what I found:

+ (NSCache *)fontCache
{
	if (!_fontCache)
	{
		_fontCache = [[NSCache alloc] init];
	}
 
	return _fontCache;
}

This is exactly the non-GCD-way that I alluded to above. This hasn’t been a problem so far because all calls to it would have come from the same thread, but while we are at its a great exercise to show that we have properly inhaled the knowledge imparted upon us above.

+ (NSCache *)fontCache
{
	static dispatch_once_t onceToken;
 
	dispatch_once(&onceToken, ^{
		_fontCache = [[NSCache alloc] init];
	});
 
	return _fontCache;
}

Ah, much better. Works just like before, but now thread-safe.

There is one riddle that I stumbled up I was not able to explain. In the same file DTFontDescriptor I also tried to adjust the second lookup table fontOverrides in the same fashion, but when trying to run the Demo it stalls somehow deadlocked. Could it be that the current way of using semaphores to synchronizing interferes with the dispatch_once?

Comment below if you know how to fix that…


Categories: Recipes

8 Comments »

  1. I usually init a static variable during the app loading for the variables that are needed for the entire life of the application:

    static NSString cachePath = nil;

    +(NSString *) cachePath {
    if (!cachePath) {
    cachePath = …..
    }
    return cachePath;
    }

  2. well, that’s the if (!something) approach that I alluded to earlier. It’s better than not caching at all, but without GCD it is not thread-safe.

  3. I don’t get why these initializations needs to be thread-safe when you can init these variables during the app loading.

  4. In the first case – caching the file system paths – you assign the static variable an autoreleased object. In the case of the fontCache, the static variable is assigned an object with a retain count of +1. Why the difference?

  5. Nice post Oliver (as usual).

    One thing, as Alan noted, I needed to add a retain to the cachedPath.
    i.e.

    [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] retain]

  6. … only if you are not yet using ARC. I am and so should you.

  7. It doesn’t have to be in your case, but it is generally smart to make it a habit. Also the point of this article was to show thread-safe initialization of a cached value.