Monday, October 30, 2006 at 6:20 PM
Posted by: Dave MacLachlan, Member of Technical Staff, Mac Team(Editor's note: today's post is a bit different from our usual fare -- it's aimed at the Mac programmers out there. And if you're not a programmer, you might want to find one to guide you through this peek behind the scenes of how we make our applications fast and reliable.)
At Google, software performance is extremely important. Every millisecond counts, which is why we spend a lot of time using performance tools and other techniques to help make our software faster. I was recently Sharking a piece of multithreaded code and realized we were getting bitten by the use of an
@synchronized
block around a shared resource we were using:
+(id)fooFerBar:(id)bar {
@synchronized(self) {
static NSDictionary *foo = nil;
if (!foo) foo = [NSDictionary dictionaryWithObjects:...];
}
return [foo objectWithKey:bar];
}
Shark told us without a doubt that we were paying heavily for the
@synchronized
block each of the millions of times we were calling fooFerBar
. We couldn't create the resource in +initialize
, because fooFerBar
was part of a category, and overriding +initialize
in a category is a bad thing. We also couldn't use +load
, because other classes could have easily called fooFerBar
in their +load
, and there's no guarantee on loading order. So our only choice was to minimize the impact of that @synchronized
block, and we didn't want to run into the infamous and dreaded double-checked locking anti-pattern.So, I wondered, how exactly does
@synchronized
work? And is there a cheaper way of getting the same thread-safe result? I disassembled the code to find out what @synchronized
does, and I saw something like this:
...
objc_sync_enter
objc_exception_try_enter
setjmp
objc_exception_extract
my actual code
objc_exception_try_exit
objc_sync_exit
...
objc_exception_throw
...
That's a lot of setup and tear-down for a simple lock around a shared resource. In this case, we don't need to be exception safe. By reading the Objective-C documentation on exception handling and thread synchronization, we learn that not only does
@synchronized
give us a lock, but it's a recursive lock, which is overkill for this particular usage.By examining the code (ADC registration required) that implements
objc_sync_enter
and obc_sync_exit
, we can see that on every @synchronized(foo)
block, we are actually paying for 3 lock/unlock sequences. objc_sync_enter
calls id2data
, which is responsible for getting the lock associated with foo
, and then locks it. objc_sync_exit
also calls id2data
to get the lock associated with foo
, and then unlocks it. And, id2data
must lock/unlock its own internal data structures so that it can safely get the lock associated with foo
, so we pay for that on each call as well.We need to do better than this. It looks like it's time to go back to basics, throw away the
@synchronized
call, and wrap our code with some pthread
locks instead.
#include <pthread.h>
+(id)fooFerBar:(id)bar {
static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
if (pthread_mutex_lock(&mtx)) {
printf("lock failed sigh...");
exit(-1);
}
static NSDictionary *foo = nil;
if (!foo) foo = [NSDictionary dictionaryWithObjects:...];
if (pthread_mutex_unlock(&mtx) != 0)) {
printf("unlock failed sigh...");
exit(-1);
}
return [foo objectWithKey:bar];
}
This is ugly stuff, but it's significantly faster, according to Shark. And fast is what we want. We've avoided setting up an exception stack, two excess locks, and a bunch of miscellaneous support code.
So we've achieved our goal of faster code that will work fine, but are there other, cleaner options? After all, if the code is cleaner, there are fewer places for bugs to hide. Tune in for our next post, wherein we'll explore that question.