ARC considered harmful ;)
Here's something I alluded to in my Dienstag Post Mortem. The title is obviously ripped off from Dijkstra
Question
There are now so many more possibilities to implement singletons, I thought it would be instructive for me to compare various implementations of +sharedInstance and to pick the best one for my future coding. As I by-product I wanted to see, how this was done in ARC vs. non-ARC. I was in for a surprise...
To recap the basics, a simple +sharedInstance method implementation in a single-threaded app looks like this:
+ (id) sharedInstance { static id sharedInstance; if( ! sharedInstance) sharedInstance = [self new]; return( sharedInstance); }
This doesn't work 100% well in multi-threaded apps, as there is a chance that more than one instance gets created, if multiple threads enter the method at pretty much the same time.
Test Scenario
Here are various strategies I came up with:
- TestSynchronizedNaive wraps the code in @synchronized
- TestLockNaive wraps the code in NSLock
- TestThreadLocal creates a thread local singleton with NSThread calls
- TestPthreadLocal does the same with pthread functionality
- TestDispatchOnce uses GCD
- TestLock wraps the code in NSLock, but more cleverly
- TestSynchronized wraps the code in @synchronized, but more cleverly
- TestInitialize uses +initialize to setup the singleton already
I made an Xcode project
mulle-singleton-comparison, that calls +sharedInstance
repeatedly and measures the time. Two shell scripts let me build
and run the (currently) 24 executables conveniently.
Results
These are the results (Numbers document) for three test scenarios. Larger numbers are worse.
The first result column contains the execution time when compiled with ARC. The results of the second columns are when compiled without ARC. The third column numbers are also without ARC and with two more optimizations enabled (just for fun). The fourth column gives the ARC pain factor, how much slower the code runs with ARC enabled.
arc, objc exceptions, std frame pointer | no arc, objc exceptions, std frame pointer | no arc, no objc exceptions, no frame pointer | ARC Painfactor | |
---|---|---|---|---|
TestSynchronizedNaive | 1,088,466 | 637,118 | 632,993 | 2 |
TestLockNaive | 823,282 | 453,874 | 458,832 | 2 |
TestThreadLocal | 1,428,187 | 438,916 | 443,269 | 3 |
TestPthreadLocal | 379,730 | 39,471 | 39,128 | 10 |
TestDispatchOnce | 374,431 | 35,851 | 30,891 | 10 |
TestLock | 363,665 | 30,882 | 28,297 | 12 |
TestSynchronized | 363,659 | 28,415 | 25,904 | 13 |
TestInitialize | 366,240 | 25,959 | 23,351 | 14 |
Discussion
Most interesting are the relationships between the measured times.
The difference between the absolute best time and the absolute worst time was 6000 %.
No matter what, everything moved at least half as fast, when ARC was enabled.
The better the code became, the more painful ARC became.
blue: ARC
green: no ARC
Moral
Magic == Pain
Post a comment
All comments are held for moderation; basic HTML formatting accepted.