Last active
February 20, 2018 06:53
-
-
Save azinman/5410263c62157086943a to your computer and use it in GitHub Desktop.
@synchronized vs pthread mutex vs NSRecursiveLock vs Semaphore
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// AppDelegate.m | |
// LockTest | |
// | |
// Created by zinman on 1/29/16. | |
// | |
#import "AppDelegate.h" | |
#import <pthread.h> | |
#import <objc/runtime.h> | |
typedef void (^VoidBlock)(void); | |
extern uint64_t dispatch_benchmark(size_t count, VoidBlock); // Private API | |
/* | |
Benchmarks locks suitable for iOS development. | |
The best lock to use is one that handles exceptions (in Obj-C and/or Swift) | |
and is re-eentrant to prevent one form of deadlocks. But which situation are we | |
likely to be in? Are we heavily-re-entrant? Are we under contention? Are we | |
simple and straightforward? | |
On an iPhone 6 with default compiler settings under Release mode: | |
Synchronized avg: 500 ns | |
Synchronized 3x avg: 809 ns | |
Synchronized contended on main avg: 1082 ns | |
Synchronized contented on high priority avg: 1096 ns | |
Pthread mutex avg: 133 ns | |
Pthread reentrant mutex avg: 194 ns | |
Pthread reentrant mutex + exceptions avg: 191 ns | |
Pthread 3x reentrant mutex + exceptions avg: 262 ns | |
Pthread reentrant mutex contended on main avg: 742 ns | |
Pthread reentrant mutex contended on high priority avg: 915 ns | |
NSRecursiveLock avg: 188 ns | |
NSRecursiveLock + exception handling avg: 179 ns | |
NSRecursiveLock + exception handling 3x avg: 348 ns | |
NSRecursiveLock under contention on main + exception handling avg: 758 ns | |
NSRecursiveLock under contention on high priority + exception handling avg: 928 ns | |
Semaphore lock + exception handling avg: 94 ns | |
Semaphore lock under contention on main avg: 239 ns | |
Semaphore lock under contention on high priority avg: 285 ns | |
*/ | |
@interface Result : NSObject | |
@property(nonatomic, strong) NSString *desc; | |
@property(nonatomic, assign) uint64_t avgTime; | |
@end | |
@implementation Result | |
@end | |
static NSObject *dummy = nil; | |
static inline void testDummy() { | |
[dummy hash]; | |
} | |
static inline void testSynchronized() { | |
@synchronized(dummy) { | |
// Prevent being compiled out | |
[dummy hash]; | |
} | |
} | |
static inline void testSynchronized3x() { | |
@synchronized(dummy) { | |
// Prevent being compiled out | |
[dummy hash]; | |
@synchronized(dummy) { | |
[dummy hash]; | |
@synchronized(dummy) { | |
[dummy hash]; | |
} | |
} | |
} | |
} | |
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; | |
static inline void testMutex() { | |
pthread_mutex_lock(&mutex); | |
[dummy hash]; | |
pthread_mutex_unlock(&mutex); | |
} | |
pthread_mutex_t reentrantMutex = PTHREAD_RECURSIVE_MUTEX_INITIALIZER; | |
static inline void testReentrantMutex() { | |
pthread_mutex_lock(&reentrantMutex); | |
[dummy hash]; | |
pthread_mutex_unlock(&reentrantMutex); | |
} | |
static inline void testReentrantMutexWithException() { | |
pthread_mutex_lock(&reentrantMutex); | |
@try { | |
[dummy hash]; | |
} | |
@finally { | |
pthread_mutex_unlock(&reentrantMutex); | |
} | |
} | |
static inline void testReentrantMutexWithException3x() { | |
pthread_mutex_lock(&reentrantMutex); | |
@try { | |
[dummy hash]; // hash | |
pthread_mutex_lock(&reentrantMutex); | |
@try { | |
[dummy hash]; // hash | |
pthread_mutex_lock(&reentrantMutex); | |
@try { | |
[dummy hash]; | |
} @finally { | |
pthread_mutex_unlock(&reentrantMutex); | |
} | |
} @finally { | |
pthread_mutex_unlock(&reentrantMutex); | |
} | |
} | |
@finally { | |
pthread_mutex_unlock(&reentrantMutex); | |
} | |
} | |
static NSRecursiveLock *recursiveLock = nil; | |
static inline void testRecursiveLock() { | |
[recursiveLock lock]; | |
[dummy hash]; | |
[recursiveLock unlock]; | |
} | |
static inline void testRecursiveLockWithException() { | |
[recursiveLock lock]; | |
@try { | |
[dummy hash]; | |
} | |
@finally { | |
[recursiveLock unlock]; | |
} | |
} | |
static inline void testRecursiveLockWithException3x() { | |
[recursiveLock lock]; // once | |
@try { | |
[dummy hash]; // hash | |
[recursiveLock lock]; // twice | |
@try { | |
[dummy hash]; // hash | |
[recursiveLock lock]; // thrice | |
@try { | |
[dummy hash]; // hash | |
} @finally { | |
[recursiveLock unlock]; // unlock thice | |
} | |
} @finally { | |
[recursiveLock unlock]; // unlock twice | |
} | |
} | |
@finally { | |
[recursiveLock unlock]; // unlock once | |
} | |
} | |
dispatch_semaphore_t semaphore = nil; | |
static inline void testSemaphoreWithException() { | |
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); | |
@try { | |
[dummy hash]; | |
} | |
@finally { | |
dispatch_semaphore_signal(semaphore); | |
} | |
} | |
typedef void(^RecordResult)(NSString *desc, VoidBlock block); | |
static NSArray<Result *>* benchmarkLocks(size_t count) { | |
// Yes these are global variables, but for this simple demo it's fine. C-functions are preferred | |
// for benchmarking because I'm hoping they'll get inlined (some may not be...) but at least | |
// we get to use much faster vtable lookups instead of obj-c's runtime for method invocations. | |
dummy = [NSObject new]; | |
[dummy hash]; // Bring into dummyc runtime method cache | |
recursiveLock = [NSRecursiveLock new]; | |
semaphore = dispatch_semaphore_create(1); | |
NSMutableArray *results = [NSMutableArray new]; | |
dispatch_queue_t highContentionQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0); | |
dispatch_semaphore_t contentionSemaphore = dispatch_semaphore_create(0); | |
uint64_t noLockAvgTime = dispatch_benchmark(count, ^{ testDummy(); }); | |
RecordResult run = ^(NSString *desc, VoidBlock block) { | |
uint64_t avgTime = dispatch_benchmark(count, block); | |
Result *result = [Result new]; | |
result.desc = desc; | |
result.avgTime = avgTime - noLockAvgTime; | |
@synchronized(results) { // We can be on high priority OR main right now. | |
[results addObject:result]; | |
} | |
NSLog(@"%@ avg: %llu ns", desc, avgTime - noLockAvgTime); | |
}; | |
// Synchronized | |
run(@"Synchronized", ^{ testSynchronized(); }); | |
run(@"Synchronized 3x", ^{ testSynchronized3x(); }); | |
// Contend lock | |
dispatch_async(highContentionQueue, ^{ | |
run(@"Synchronized contented on high priority", ^{ testSynchronized(); }); | |
dispatch_semaphore_signal(contentionSemaphore); | |
}); | |
run(@"Synchronized contended on main", ^{ testSynchronized(); }); | |
dispatch_semaphore_wait(contentionSemaphore, DISPATCH_TIME_FOREVER); | |
// Pthread | |
run(@"Pthread mutex", ^{ testMutex(); }); | |
run(@"Pthread reentrant mutex", ^{ testReentrantMutex(); }); | |
run(@"Pthread reentrant mutex + exceptions", ^{ testReentrantMutexWithException(); }); | |
run(@"Pthread 3x reentrant mutex + exceptions", ^{ testReentrantMutexWithException3x(); }); | |
// Contend lock | |
dispatch_async(highContentionQueue, ^{ | |
run(@"Pthread reentrant mutex contended on high priority", ^{ testReentrantMutexWithException(); }); | |
dispatch_semaphore_signal(contentionSemaphore); | |
}); | |
run(@"Pthread reentrant mutex contended on main", ^{ testReentrantMutexWithException(); }); | |
dispatch_semaphore_wait(contentionSemaphore, DISPATCH_TIME_FOREVER); | |
// NSRecursiveLock | |
run(@"NSRecursiveLock", ^{ testRecursiveLock(); }); | |
run(@"NSRecursiveLock + exception handling", ^{ testRecursiveLockWithException(); }); | |
run(@"NSRecursiveLock + exception handling 3x", ^{ testRecursiveLockWithException3x(); }); | |
// Contend lock | |
dispatch_async(highContentionQueue, ^{ | |
run(@"NSRecursiveLock under contention on high priority + exception handling", ^{ testReentrantMutexWithException(); }); | |
dispatch_semaphore_signal(contentionSemaphore); | |
}); | |
run(@"NSRecursiveLock under contention on main + exception handling", ^{ testReentrantMutexWithException(); }); | |
dispatch_semaphore_wait(contentionSemaphore, DISPATCH_TIME_FOREVER); | |
// Semaphore -- we get that exception handling is cheap now and can imagine without. | |
run(@"Semaphore lock + exception handling", ^{ testSemaphoreWithException(); }); | |
dispatch_async(highContentionQueue, ^{ | |
run(@"Semaphore lock under contention on high priority", ^{ testSemaphoreWithException(); }); | |
dispatch_semaphore_signal(contentionSemaphore); | |
}); | |
run(@"Semaphore lock under contention on main", ^{ testSemaphoreWithException(); }); | |
dispatch_semaphore_wait(contentionSemaphore, DISPATCH_TIME_FOREVER); | |
// Clean up | |
pthread_mutex_destroy(&mutex); | |
pthread_mutex_destroy(&reentrantMutex); | |
dummy = nil; | |
semaphore = nil; | |
recursiveLock = nil; | |
return results; | |
} | |
@interface AppDelegate () | |
@property(nonatomic, strong) NSArray <Result *>*results; | |
@end | |
@implementation AppDelegate | |
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { | |
UIWindow *mainWindow = application.windows[0]; | |
UIView *mainView = mainWindow.rootViewController.view; | |
UILabel *l = [[UILabel alloc] initWithFrame:mainView.bounds]; | |
l.text = @"1 min please"; | |
l.textAlignment = NSTextAlignmentCenter; | |
l.font = [UIFont systemFontOfSize:48]; | |
[mainView addSubview:l]; | |
// Let the label show | |
dispatch_async(dispatch_get_main_queue(), ^{ | |
size_t count = 10000000; | |
self.results = benchmarkLocks(count); | |
UITableView *tableView = [[UITableView alloc] initWithFrame: | |
CGRectMake(0, 20, mainView.bounds.size.width, mainView.bounds.size.height - 20)]; | |
tableView.dataSource = (id<UITableViewDataSource>)self; | |
tableView.alpha = 0; | |
[mainView addSubview:tableView]; | |
[tableView reloadData]; | |
[UIView animateWithDuration:0.35 animations:^{ | |
tableView.alpha = 1; | |
l.alpha = 0; | |
}]; | |
}); | |
return YES; | |
} | |
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { | |
return self.results.count; | |
} | |
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { | |
static NSString *identifier = @"benchmarkCell"; | |
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier]; | |
if (!cell) { | |
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:identifier]; | |
} | |
// Won't worry about crash since we know its ok in this test | |
if (self.results) { | |
Result *r = self.results[indexPath.row]; | |
cell.textLabel.text = r.desc; | |
cell.detailTextLabel.text = [NSString stringWithFormat:@"Avg %llu ns", r.avgTime]; | |
} | |
return cell; | |
} | |
@end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment