Note that there are some explanatory texts on larger screens.

plurals
  1. PO
    primarykey
    data
    text
    <p>For posterity, lest there be any doubt about this, let's consider the following test harness, built in a simple empty application template. It does 1000 operations using each mechanism, and also has a run loop observer, so we can see how our enqueued asynchronous tasks relate to the spinning of the main run loop. It logs to console, but does so asynchronously, so that the cost of <code>NSLog</code> isn't confounding our measurement. It also intentionally blocks the main thread while it enqueues <code>NSOperations</code>/<code>dispatch_asyncs</code>/<code>performSelectors</code> tasks, so that the act of enqueuing also doesn't interfere. Here's the code:</p> <pre><code>#import "NSAppDelegate.h" dispatch_queue_t gLogQueue; #define NSLogAsync(...) dispatch_async(gLogQueue, ^{ NSLog(__VA_ARGS__); }); @implementation NSAppDelegate { dispatch_group_t g; NSUInteger numOps; useconds_t usleepDuration; } static void MyCFRunLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info); - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { // parameters of test numOps = 1000; usleepDuration = 1000; // Set up a serial queue so we can keep the cost of calling NSLog more or less out of our test case. gLogQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL); // Group allows us to wait for one test to finish before the next one begins g = dispatch_group_create(); // Insert code here to initialize your application CFRunLoopObserverRef rlo = CFRunLoopObserverCreate(NULL, kCFRunLoopAllActivities, YES, 0, MyCFRunLoopObserverCallBack, NULL); CFRunLoopAddObserver(CFRunLoopGetCurrent(), rlo, kCFRunLoopCommonModes); CFRelease(rlo); NSCondition* cond = [[NSCondition alloc] init]; dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSTimeInterval start = 0, end = 0; // pause the main thread dispatch_async(dispatch_get_main_queue(), ^{ [cond lock]; [cond signal]; [cond wait]; [cond unlock]; }); // wait for the main thread to be paused [cond lock]; [cond wait]; // NSOperationQueue for (NSUInteger i = 0; i &lt; numOps; ++i) { dispatch_group_enter(g); [[NSOperationQueue mainQueue] addOperationWithBlock:^{ NSLogAsync(@"NSOpQ task #%@", @(i)); usleep(usleepDuration); // simulate work dispatch_group_leave(g); }]; } // unpause the main thread [cond signal]; [cond unlock]; // mark start time start = [NSDate timeIntervalSinceReferenceDate]; // wait for it to be done dispatch_group_wait(g, DISPATCH_TIME_FOREVER); end = [NSDate timeIntervalSinceReferenceDate]; NSTimeInterval opQDuration = end - start; NSLogAsync(@"NSOpQ took: %@s", @(opQDuration)); // pause the main thread dispatch_async(dispatch_get_main_queue(), ^{ [cond lock]; [cond signal]; [cond wait]; [cond unlock]; }); // wait for the main thread to be paused [cond lock]; [cond wait]; // Dispatch_async for (NSUInteger i = 0; i &lt; numOps; ++i) { dispatch_group_enter(g); dispatch_async(dispatch_get_main_queue(), ^{ NSLogAsync(@"dispatch_async main thread task #%@", @(i)); usleep(usleepDuration); // simulate work dispatch_group_leave(g); }); } // unpause the main thread [cond signal]; [cond unlock]; // mark start start = [NSDate timeIntervalSinceReferenceDate]; dispatch_group_wait(g, DISPATCH_TIME_FOREVER); end = [NSDate timeIntervalSinceReferenceDate]; NSTimeInterval asyncDuration = end - start; NSLogAsync(@"dispatch_async took: %@s", @(asyncDuration)); // pause the main thread dispatch_async(dispatch_get_main_queue(), ^{ [cond lock]; [cond signal]; [cond wait]; [cond unlock]; }); // wait for the main thread to be paused [cond lock]; [cond wait]; // performSelector: for (NSUInteger i = 0; i &lt; numOps; ++i) { dispatch_group_enter(g); [self performSelectorOnMainThread: @selector(selectorToPerfTask:) withObject: @(i) waitUntilDone: NO]; } // unpause the main thread [cond signal]; [cond unlock]; // mark start start = [NSDate timeIntervalSinceReferenceDate]; dispatch_group_wait(g, DISPATCH_TIME_FOREVER); end = [NSDate timeIntervalSinceReferenceDate]; NSTimeInterval performDuration = end - start; NSLogAsync(@"performSelector took: %@s", @(performDuration)); // Done. dispatch_async(dispatch_get_main_queue(), ^{ CFRunLoopRemoveObserver(CFRunLoopGetCurrent(), rlo, kCFRunLoopCommonModes); NSLogAsync(@"Done. NSOperationQueue: %@s dispatch_async: %@s performSelector: %@", @(opQDuration), @(asyncDuration), @(performDuration)); }); }); } - (void)selectorToPerfTask: (NSNumber*)task { NSLogAsync(@"performSelector task #%@", task); usleep(usleepDuration); // simulate work dispatch_group_leave(g); } static NSString* NSStringFromCFRunLoopActivity(CFRunLoopActivity activity) { NSString* foo = nil; switch (activity) { case kCFRunLoopEntry: foo = @"kCFRunLoopEntry"; break; case kCFRunLoopBeforeTimers: foo = @"kCFRunLoopBeforeTimers"; break; case kCFRunLoopBeforeSources: foo = @"kCFRunLoopBeforeSources"; break; case kCFRunLoopBeforeWaiting: foo = @"kCFRunLoopBeforeWaiting"; break; case kCFRunLoopAfterWaiting: foo = @"kCFRunLoopAfterWaiting"; break; case kCFRunLoopExit: foo = @"kCFRunLoopExit"; break; default: foo = @"ERROR"; break; } return foo; } static void MyCFRunLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) { NSLogAsync(@"RLO: %@", NSStringFromCFRunLoopActivity(activity)); } @end </code></pre> <p>In the output of this code, we see the following (with irrelevant/repeating portions removed):</p> <pre><code>RLO: kCFRunLoopEntry RLO: kCFRunLoopBeforeTimers RLO: kCFRunLoopBeforeSources RLO: kCFRunLoopBeforeWaiting RLO: kCFRunLoopAfterWaiting NSOpQ task #0 RLO: kCFRunLoopExit RLO: kCFRunLoopEntry RLO: kCFRunLoopBeforeTimers RLO: kCFRunLoopBeforeSources RLO: kCFRunLoopBeforeWaiting RLO: kCFRunLoopAfterWaiting NSOpQ task #1 RLO: kCFRunLoopExit ... pattern repeats ... RLO: kCFRunLoopEntry RLO: kCFRunLoopBeforeTimers RLO: kCFRunLoopBeforeSources RLO: kCFRunLoopBeforeWaiting RLO: kCFRunLoopAfterWaiting NSOpQ task #999 RLO: kCFRunLoopExit NSOpQ took: 1.237247049808502s RLO: kCFRunLoopEntry RLO: kCFRunLoopBeforeTimers RLO: kCFRunLoopBeforeSources RLO: kCFRunLoopBeforeWaiting RLO: kCFRunLoopAfterWaiting dispatch_async main thread task #0 dispatch_async main thread task #1 ... pattern repeats ... dispatch_async main thread task #999 dispatch_async took: 1.118762016296387s RLO: kCFRunLoopExit RLO: kCFRunLoopEntry RLO: kCFRunLoopBeforeTimers RLO: kCFRunLoopBeforeSources performSelector task #0 performSelector task #1 ... pattern repeats ... performSelector task #999 performSelector took: 1.133482992649078s RLO: kCFRunLoopExit RLO: kCFRunLoopEntry RLO: kCFRunLoopBeforeTimers RLO: kCFRunLoopBeforeSources Done. NSOperationQueue: 1.237247049808502s dispatch_async: 1.118762016296387s performSelector: 1.133482992649078 </code></pre> <p>What this shows us is that <code>NSOperation</code>s enqueued on the main queue get executed one per pass of the run loop. (Incidentally, this is going to allow views to draw for each operation, so if you're updating a UI control in these tasks as the OP was, this will allow them to draw.) With <code>dispatch_async(dispatch_get_main_queue(),...)</code> and <code>-[performSelectorOnMainThread:...]</code> all enqueued blocks/selectors are called one after the other without letting views draw or anything like that. (If you don't forcibly pause the main runloop while you enqueue tasks, you <em>sometimes</em> can see the run loop spin once or twice during the enqueueing process.)</p> <p>In the end, the results are about what I expected they would be:</p> <ul> <li>NSOperationQueue: 1.2372s </li> <li>dispatch_async: 1.1188s</li> <li>performSelector: 1.1335s</li> </ul> <p><code>NSOperationQueue</code> will always be slower, because spinning the run loop isn't free. In this test harness, the run loop isn't even <em>doing</em> anything of substance, and it's already 10% slower than <code>dispatch_async</code>. If it were doing anything of substance, like redrawing a view, it would be <em>much</em> slower. As for <code>dispatch_async</code> vs <code>performSelectorOnMainThread:</code> both execute all enqueued items in one spin of the run loop, so the difference is pretty marginal. I expect it's down to message send overhead and managing the retain/releases on the target and argument of the <code>performSelector...</code>.</p> <p>So contrary to the implication of the question, <code>NSOperationQueue</code> is not, objectively, the fastest of the three mechanisms, but rather the slowest. My suspicion is that in OP's case, <code>NSOperationQueue</code> <em>appears</em> faster because its "time to first visible change" is going to be much shorter, whereas for <code>dispatch_async</code> and <code>performSelector</code> all enqueued operations are going to be executed, and only <em>then</em> will the view redraw and show the new state. In the pathological case, I would expect this to mean that only the last frame was ever seen, although if you don't block the main thread while enqueueing, you could get a handful of visible frames (but you'll effectively be dropping most of the frames on the ground.)</p> <p>Regardless of which asynchronous execution mechanism is objectively fastest, they're <em>all</em> pretty crappy ways to do animation.</p>
    singulars
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    plurals
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. This table or related slice is empty.
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload