哇。好的——我最初的绩效评估完全错误。给我涂上愚蠢的颜色。
没那么傻。我的性能测试是错误的。固定的。以及深入研究 GCD 代码。
更新:可以在此处找到基准代码:https ://github.com/bbum/StackOverflow 希望它现在是正确的。:)
更新 2:添加了每种测试的 10 队列版本。
好的。重写答案:
• <code>@synchronized() 已经存在很长时间了。它被实现为哈希查找以查找然后锁定的锁。它“相当快”——通常足够快——但在高竞争下可能是一种负担(任何同步原语都可能如此)。
•dispatch_sync()
不一定需要锁,也不需要复制块。具体来说,在快速路径的情况下,dispatch_sync()
将直接在调用线程上调用块,而不复制块。即使在慢速路径的情况下,也不会复制块,因为调用线程必须阻塞直到执行(调用线程被挂起,直到dispatch_sync()
完成之前的任何工作,然后线程恢复)。一个例外是在主队列/线程上调用;在这种情况下,该块仍然没有被复制(因为调用线程被挂起,因此,使用堆栈中的一个块是可以的),但是有很多工作完成了在主队列中排队、执行和然后恢复调用线程。
• <code>dispatch_async() 要求复制该块,因为它不能在当前线程上执行,也不能阻塞当前线程(因为该块可能会立即锁定某些仅在代码行上可用的线程本地资源在dispatch_async()
. 之后虽然开销dispatch_async()
很大,但将工作移出当前线程,使其立即恢复执行。
最终结果 -dispatch_sync()
比 快@synchronized
,但不是一般有意义的数量(在 '12 iMac 和 '11 mac mini 上 - 两者之间的#s 非常不同,顺便说一句......并发的乐趣)。在无竞争的情况下,使用dispatch_async()
速度比两者都慢,但不会慢很多。但是,当资源处于争用状态时,使用“dispatch_async()”会明显更快。
@synchronized uncontended add: 0.14305 seconds
Dispatch sync uncontended add: 0.09004 seconds
Dispatch async uncontended add: 0.32859 seconds
Dispatch async uncontended add completion: 0.40837 seconds
Synchronized, 2 queue: 2.81083 seconds
Dispatch sync, 2 queue: 2.50734 seconds
Dispatch async, 2 queue: 0.20075 seconds
Dispatch async 2 queue add completion: 0.37383 seconds
Synchronized, 10 queue: 3.67834 seconds
Dispatch sync, 10 queue: 3.66290 seconds
Dispatch async, 2 queue: 0.19761 seconds
Dispatch async 10 queue add completion: 0.42905 seconds
用一粒盐把上面的东西拿走;它是最差的一种微型基准,因为它不代表任何现实世界的常见使用模式。“工作单元”如下,上面的执行次数代表 1,000,000 次执行。
- (void) synchronizedAdd:(NSObject*)anObject
{
@synchronized(self) {
[_a addObject:anObject];
[_a removeLastObject];
_c++;
}
}
- (void) dispatchSyncAdd:(NSObject*)anObject
{
dispatch_sync(_q, ^{
[_a addObject:anObject];
[_a removeLastObject];
_c++;
});
}
- (void) dispatchASyncAdd:(NSObject*)anObject
{
dispatch_async(_q, ^{
[_a addObject:anObject];
[_a removeLastObject];
_c++;
});
}
(_c 在每次通过开始时重置为 0,并在最后断言为 == 到 # 个测试用例,以确保代码在喷出时间之前实际上正在执行所有工作。)
对于无争议的情况:
start = [NSDate timeIntervalSinceReferenceDate];
_c = 0;
for(int i = 0; i < TESTCASES; i++ ) {
[self synchronizedAdd:o];
}
end = [NSDate timeIntervalSinceReferenceDate];
assert(_c == TESTCASES);
NSLog(@"@synchronized uncontended add: %2.5f seconds", end - start);
对于竞争的 2 个队列,案例(q1 和 q2 是串行的):
#define TESTCASE_SPLIT_IN_2 (TESTCASES/2)
start = [NSDate timeIntervalSinceReferenceDate];
_c = 0;
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
dispatch_apply(TESTCASE_SPLIT_IN_2, serial1, ^(size_t i){
[self synchronizedAdd:o];
});
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
dispatch_apply(TESTCASE_SPLIT_IN_2, serial2, ^(size_t i){
[self synchronizedAdd:o];
});
});
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
end = [NSDate timeIntervalSinceReferenceDate];
assert(_c == TESTCASES);
NSLog(@"Synchronized, 2 queue: %2.5f seconds", end - start);
对于每个工作单元变体,上述内容只是简单地重复(没有使用棘手的运行时魔法;copypasta FTW!)。
考虑到这一点:
•@synchronized()
如果您喜欢它的外观,请使用。现实情况是,如果您的代码在该数组上竞争,您可能遇到了架构问题。 注意: using@synchronized(someObject)
可能会产生意想不到的后果,因为如果对象内部使用它可能会导致额外的争用@synchronized(self)
!
•dispatch_sync()
如果您愿意,请与串行队列一起使用。没有开销——在竞争和非竞争的情况下它实际上都更快——并且使用队列更容易调试和更容易分析,因为 Instruments 和 Debugger 都有用于调试队列的优秀工具(而且它们正在变得更好一直),而调试锁可能会很痛苦。
•dispatch_async()
与不可变数据一起用于竞争激烈的资源。IE:
- (void) addThing:(NSString*)thing {
thing = [thing copy];
dispatch_async(_myQueue, ^{
[_myArray addObject:thing];
});
}
最后,使用哪一个来维护数组的内容并不重要。同步案例的争用成本非常高。对于异步情况,争用的成本会大大降低,但复杂性或奇怪的性能问题的可能性会大大增加。
在设计并发系统时,最好保持队列之间的边界尽可能小。其中很大一部分是确保尽可能少的资源“存在”在边界的两侧。