This article
is an attempt to sum up a small number of generic rules that appear to be
useful rules of thumb when creating high performing programs. It is structured
by first establishing some fundamental causes of performance hits followed by
their extensions.
Two fundamental
causes of performance problems
- Memory
Latency: A big
performance problem on modern computers is the latency of SDRAM. The CPU waits
idly for a read from memory to come back.
- Context
Switching: When a
CPU switches context "the memory it will access is most likely unrelated
to the memory, the previous context was accessing. This often results in
significant eviction of the previous cache, and requires the switched-to
context to load much of its data from RAM, which is slow."
Rules to help
balance the forces of evil
- Batch
work: To avoid the
cost of context switches, it makes sense to try to invoke them as rarely as
possible. You may not have much control over operating systems’ system calls. Avoid
context switching by batching work. For example, there are vector versions of
system calls like writev() and readv() that operate on more than one item per
call. An implication is that you want to merge as many writes as possible.
- Avoid
Magic Numbers: They
don't scale. Waking a thread up every 100ms or when 100 jobs are queued, or
using fixed size buffers, doesn't adapt to changing circumstances.
- Allocate
memory buffers up front: Avoid
extra copying and maintain predictable memory usage.
- Organically adapt your job-batching sizes to the granularity of the scheduler and the time it takes for your thread to wake up.
- Adapt receive buffer sizes for sockets, while at the same time avoiding copying memory out of the kernel.
- Always complete all work queued up for a thread before going back to sleep.
- Only signal a worker thread to wake up when the number of jobs on its queue go from 0 to > 0. Any other signal is redundant and a waste of time.