Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save phucledien/20a0e2497b7926591d2f9b58b7607dbc to your computer and use it in GitHub Desktop.
Save phucledien/20a0e2497b7926591d2f9b58b7607dbc to your computer and use it in GitHub Desktop.
Making efficient use of the libdispatch (GCD)

libdispatch efficiency tips

The libdispatch is one of the most misused API due to the way it was presented to us when it was introduced and for many years after that, and due to the confusing documentation and API. This page is a compilation of important things to know if you're going to use this library. Many references are available at the end of this document pointing to comments from Apple's very own libdispatch maintainer (Pierre Habouzit).

My take-aways are:

  • You should create very few, long-lived, well-defined queues. These queues should be seen as execution contexts in your program (gui, background work, ...) that benefit from executing in parallel. An important thing to note is that if these queues are all active at once, you will get as many threads running. In most apps, you probably do not need to create more than 3 or 4 queues.

  • Go serial first, and as you find performance bottle necks, measure why, and if concurrency helps, apply with care, always validating under system pressure. Reuse queues by default and add more if there's some measurable benefit to it. Do not attempt to go wide by default.

  • Queues that target other (non-global) queues are fine (created via DispatchQueue(label:target:)), these are the ones which scale and you can have many of those (the main point is having different labels).

  • Don't use DispatchQueue.global(). Global queues easily lead to thread explosion: threads blocking on sleeps/waits/locks are considered inactive by the libdispatch which in turn will spawn new threads when other parts of your program dispatch. Note that it is impossible to guarantee that your threads are never going to block, as merely using the system libraries will cause it to happen. Global queues also do not play nice with qos/priorities. The libdispatch maintainer at Apple declared it "the worst thing that the dispatch API provides". Run your code on one of your custom queue instead (one of your well-defined execution context).

  • Concurrent queues are not as optimized as serial queues. Use them if you measure a performance improvement, otherwise it's likely premature optimization.

  • queue.async() is wasteful if the dispatched block is small (< 1ms), as it will most likely require a new thread due to libdispatch's overcommit behavior. Prefer locking to protect shared state (rather than switching the execution context).

  • Some classes/libraries are better designed as synchronous APIs, reusing the execution context from their callers/clients (instead of creating their own private queues which can lead to terrible performance). That means using traditional locking for thread-safety. os_unfair_lock is usually the fastest lock on the system (nicer with priorities, less context switches).

  • Do not block the current thread waiting on a semaphore or dispatch group after dispatching work to a background thread. This is inefficient and obviously does not scale. Rather, continue work asynchronously in a completion handler that is executed once the background work ends or just do the work synchronously.

  • Do not use DispatchQueue.main in non-GUI programs and frameworks. Per the <dispatch/queue.h> header: "Because the main queue doesn't behave entirely like a regular serial queue, it may have unwanted side-effects when used in processes that are not UI apps (daemons). For such processes, the main queue should be avoided".

  • If running concurrently, your work items need not to contend, else your performance sinks dramatically. Contention takes many forms. Locks are obvious, but it really means use of shared resources that can be a bottle neck: IPC/daemons, malloc (lock), shared memory, I/O, ...

  • You don't need to be async all the way to avoid thread explosion. Using a limited number of bottom queues and not using DispatchQueue.global() is a better fix.

  • The complexity (and bugs) of heavy async/callback designs also cannot be ignored. Synchronous code remains much easier to read, write and maintain.

  • Utilizing more than 3-4 cores isn't something that is easy, most people who try actually do not scale and waste energy for a modicum performance win. It doesn't help that CPUs have thermal issues if you ramp up, e.g. Intel will turn off turbo-boost if you use enough cores.

  • Measure the real-world performance of your product to make sure you are actually making it faster and not slower. Be very careful with micro benchmarks (they hide cache effects and keep thread pools hot), you should always have a macro benchmark to validate what you're doing.

  • libdispatch is efficient but not magic. Resources are not infinite. You cannot ignore the reality of the underlying operating system and hardware you're running on. Not all code is prone to parallelization.

@tclementdev

References

This long discussion on the swift-evolution mailing-list started it all (look for Pierre Habouzit).

Use very few queues

Go serial first

Don't use global queues

Beware of concurrent queues

Don't use async to protect shared state

Don't use async for small tasks

Some classes/libraries should just be synchronous

Contention is a performance killer for concurrency

To avoid deadlocks, use locks to protect shared state

Don't use semaphores to wait for asynchronous work

Synchronous IPC is not bad

The NSOperation API has some serious performance pitfalls

Avoid micro-benchmarking

Resources are not infinite

Background QOS work is paused when low-power mode is enabled

About dispatch_async_and_wait()

Utilizing more than 3-4 cores isn't something that is easy

A lot of iOS 12 perf wins were from daemons going single-threaded

This page is the real deal

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment