Skip to content

Instantly share code, notes, and snippets.

@azinman
Last active February 20, 2018 06:53
Show Gist options
  • Save azinman/5410263c62157086943a to your computer and use it in GitHub Desktop.
Save azinman/5410263c62157086943a to your computer and use it in GitHub Desktop.
@synchronized vs pthread mutex vs NSRecursiveLock vs Semaphore
//
// 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