|
A good lessons on performance enhancement: http://googlemac.blogspot.com/2006/10/synchronized-swimming.html @synchronized swimmingMonday, October 30, 2006 at 6:20 PMPosted 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: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. Links to this post
|

