Académique Documents
Professionnel Documents
Culture Documents
Machine Architecture
(Things Your Programming Language Never Told You)
Herb Sutter
Machine Architecture
High-level languages insulate the programmer from the machine.
That’s a wonderful thing -- except when it obscures the answers to
the fundamental questions of “What does the program do?” and
“How much does it cost?”
The C++ and C# programmer is less insulated than most, and still we
find that programmers are consistently surprised at what simple
code actually does and how expensive it can be -- not because of
any complexity of a language, but because of being unaware of the
complexity of the machine on which the program actually runs.
This talk examines the “real meanings” and “true costs” of the code
we write and run especially on commodity and server systems, by
delving into the performance effects of bandwidth vs. latency
limitations, the ever-deepening memory hierarchy, the changing
costs arising from the hardware concurrency explosion,
memory model effects all the way from the compiler to the CPU to
the chipset to the cache, and more -- and what you can do about
them.
Transfer Control
Transfer reg. A to
Storage
Latency Lags
Bandwidth
Relative Bandwidth Improvement
L = no contention
BW = best-case
Source: David Patterson, UC Berkeley,
Relative Latency Improvement HPEC keynote, Oct 2004
(http://www.ll.mit.edu/HPEC/agendas/p
roc04/invited/patterson_keynote.pdf)
Note: Processor biggest, memory smallest
Memory
CPU
Disk
L3$
DRAM
UMA
On other
Memory
nodes
NUMA
Flash
Flash Flash
Network …
Little’s Law
B L C
* But you have to provide said other work (e.g., software threads) or this is useless!
Processor Pipelining
Why do this (one instruction at a time):
Fetch Decode Execute Mem Acc Write Fetch Decode Execute
Sample
Modern CPU
Original Itanium 2 had
211Mt, 85% for cache:
16 KB L1I$
16 KB L1D$
256 KB L2$
3 MB L3$
1% of die to compute,
99% to move/store data?
Itanium 2 9050:
Dual-core
24 MB L3$
TPC-C profile
2200
(courtesy Ravi Rajwar, Intel Research)
UL2 Miss UOP and C y c le A llo c a te d
2100 dependents
C y c le E xe c u te d
2000
time
1900
C y c le
1800 Alloc
Miss Latency resumes
1700
1500
R e tire d U o p ID
Memory Latency
Is the Root of Most Evil
The vast majority of your hardware’s
complexity is a direct result of ever more
heroic attempts to hide the Memory Wall.
In CPUs, chipsets, memory subsystems, and disk subsystems.
In making the memory hierarchy ever deeper (e.g., flash
thumbdrives used by the OS for more caching; flash memory
embedded directly on hard drives).
Hardware is sometimes even willing to change the meaning of
your code, and possibly break it, just to hide memory latency
and make the code run faster.
Latency as the root of most evil is a unifying theme of this
talk, and many other talks.
Instruction Reordering
and the Memory Model
Definitions:
Instruction reordering: When a program executes instructions,
especially memory reads and writes, in an order that is
different than the order specified in the program’s source
code.
Memory model: Describes how memory reads and writes may
appear to be executed relative to their program order.
“Compilers, chips, and caches, oh my!”
Affects the valid optimizations that can be performed by
compilers, physical processors, and caches.
Processor 1 Processor 2
flag1 = 1; (1) Write 1 flag2 = 1; (2) Write 1
to flag1 to flag2
if( flag2 != 0 ) {…}; (3) (sent to if( flag1 != 0 ) {…} (4) (sent to
store store
buffer) buffer)
Global Memory
Transformations:
Reordering + invention + removal
The level at which the trans-
formation happens is (usually)
invisible to the programmer.
The only thing that matters
to the programmer is that
a correctly synchronized
program behaves as though:
The order in which memory
operations are actually executed
is equivalent to some sequential
execution according to program
source order.
Each write is visible to all
processors at the same time.
Tools and hardware (should) try
to maintain that illusion.
Sometimes they don’t. We’ll
see why, and what you can do.
A General Pattern
Consider (where x is a shared variable):
if( cond )
lock x
...
if( cond )
use x
...
if( cond )
unlock x
Q: Is this pattern safe?
A: In theory, yes. In reality, maybe not…
Speculation
Consider (where x is a shared variable):
if( cond )
x = 42;
Say the system (compiler, CPU, cache, …) speculates (predicts,
guesses, measures) that cond (may be, will be, often is) true.
Can this be transformed to:
r1 = x; // read what’s there
x = 42; // perform an optimistic write
if( !cond ) // check if we guessed wrong
x = r1; // oops: back-out write is not SC
In theory, No… but on some implementations, Maybe.
Same key issue: Inventing a write to a location that would never be
written to in an SC execution.
If this happens, it can break patterns that conditionally take a lock.
Register Allocation
Here’s a much more common problem case:
void f( /*...params...*/, bool doOptionalWork ) {
if( doOptionalWork ) xMutex.lock();
for( ... )
if( doOptionalWork ) ++x; // write is conditional
if( doOptionalWork ) xMutex.unlock();
}
A very likely (if deeply flawed) transformation:
r1 = x;
for( ... )
if( doOptionalWork ) ++r1;
x = r1; // oops: write is not conditional
If so, again, it’s not safe to have a conditional lock.
Adding 1M ints
Q: Is it faster to sum an array of ints, an equivalent
doubly-linked list of ints, or an equivalent set (tree) of
ints? Which, how much so, and why?
The NWCPP
Foundation for Justice
Authorized
Version
SPILLER
V LS On higher curves
= slower.
Adding 1M ints
Q: Is it faster to sum an array of ints, an equivalent list of ints,
or an equivalent set of ints? Which, how much so, and why?
A1: Size matters. (Smaller = further left on the line.)
Fewer linked list items fit in any given level of cache. All those object
headers and links waste space.
A2: Traversal order matters. (Linear = on a lower line.)
Takes advantage of prefetching (as discussed).
Takes advantage of out-of-order processors: A modern out-of-order
processor can potentially zoom ahead and make progress on several
items in the array at the same time. In contrast, with the linked list or
the set, until the current node is in cache, the processor can’t get
started fetching the next link to the node after that.
It’s not uncommon for a loop doing multiplies/adds on a list of
ints to be spending most its time idling, waiting for memory…