Which is a better write barrier on x86: lock+addl or xchgl?
Quoting from the IA32 manuals (Vol 3A, Chapter 8.2: Memory Ordering):
In a single-processor system for memory regions defined as write-back cacheable, the memory-ordering model respects the following principles [..]
- Reads are not reordered with other reads
- Writes are not reordered with older reads
- Writes to memory are not reordered with other writes, with the exception of
- writes executed with the
CLFLUSH
instruction- streaming stores (writes) executed with the non-temporal move instructions ([list of instructions here])
- string operations (see Section 8.2.4.1)
- Reads may be reordered with older writes to different locations but not with older writes to the same location.
- Reads or writes cannot be reordered with I/O instructions, locked instructions, or serializing instructions
- Reads cannot pass
LFENCE
andMFENCE
instructions- Writes cannot pass
SFENCE
andMFENCE
instructions
Note: The "In a single-processor system" above is slightly misleading. The same rules hold for each (logical) processor individually; the manual then goes on to describe the additional ordering rules between multiple processors. The only bit about it pertaining to the question is that
- Locked instructions have a total order.
In short, as long as you're writing to write-back memory (which is all memory you'll ever see as long as you're not a driver or graphics programmer), most x86 instructions are almost sequentially consistent - the only reordering an x86 CPU can perform is reorder later (independent) reads to execute before writes. The main thing about the write barriers is that they have a lock
prefix (implicit or explicit), which forbids all reordering and ensures that the operations is seen in the same order by all processors in a multi-processor system.
Also, in write-back memory, reads are never reordered, so there's no need for read barriers. Recent x86 processors have a weaker memory consistency model for streaming stores and write-combined memory (commonly used for mapped graphics memory). That's where the various fence
instructions come into play; they're not necessary for any other memory type, but some drivers in the Linux kernel do deal with write-combined memory so they just defined their read-barrier that way. The list of ordering model per memory type is in Section 11.3.1 in Vol. 3A of the IA-32 manuals. Short version: Write-Through, Write-Back and Write-Protected allow speculative reads (following the rules as detailed above), Uncachable and Strong Uncacheable memory has strong ordering guarantees (no processor reordering, reads/writes are immediately executed, used for MMIO) and Write Combined memory has weak ordering (i.e. relaxed ordering rules that need fences).
The "lock; addl $0,0(%%esp)" is faster in case that we testing the 0 state of lock variable at (%%esp) address. Because we add 0 value to lock variable and the zero flag is set to 1 if the lock value of variable at address (%%esp) is 0.
lfence from Intel datasheet:
Performs a serializing operation on all load-from-memory instructions that were issued prior the LFENCE instruction. This serializing operation guarantees that every load instruction that precedes in program order the LFENCE instruction is globally visible before any load instruction that follows the LFENCE instruction is globally visible.
(Editor's note: mfence
or a lock
ed operation is the only useful fence (after a store) for sequential consistency. lfence
does not block StoreLoad reordering by the store buffer.)
For instance: memory write instruction like 'mov' are atomic (they don't need lock prefix) if there are properly aligned. But this instruction is normally executed in CPU cache and will not be globally visible at this moment for all other threads, because memory fence must be performed first to make this thread wait until previous stores are visible to other threads.
So the main difference between these two instructions is that xchgl instruction will not have any effect on the conditional flags. Certainly we can test the lock variable state with lock cmpxchg instruction but this is still more complex than with lock add $0 instruction.
lock addl $0, (%esp)
is a substitute for mfence
, not lfence
.
The use-case is when you need to block StoreLoad reordering (the only kind that x86's strong memory model allows), but you don't need an atomic RMW operation on a shared variable. https://preshing.com/20120515/memory-reordering-caught-in-the-act/
e.g. assuming aligned std::atomic<int> a,b
:
movl $1, a a = 1; Atomic for aligned a
# barrier needed here
movl b, %eax tmp = b; Atomic for aligned b
Your options are:
- Do a sequential-consistency store with
xchg
, e.g.mov $1, %eax
/xchg %eax, a
so you don't need a separate barrier; it's part of the store. I think this is the most efficient option on most modern hardware; C++11 compilers other than gcc usexchg
for seq_cst stores. - Use
mfence
as a barrier. (gcc usesmov
+mfence
for seq_cst stores). Use
lock addl $0, (%esp)
as a barrier. Anylock
ed instruction is a full barrier. Does lock xchg have the same behavior as mfence?(Or to some other location, but the stack is almost always private and hot in L1d, so it's a somewhat good candidate. However this may create a dependency chain for something using the data at the bottom of the stack.)
You can only use xchg
as a barrier by folding it into a store because it unconditionally writes the memory location with a value that doesn't depend on the old value.
When possible, using xchg
for a seq-cst store is probably best, even though it also reads from the shared location. mfence
is slower than expected on recent Intel CPUs (Are loads and stores the only instructions that gets reordered?), also blocking out-of-order execution of independent non-memory instructions the same way lfence
does.
It might even be worth using lock addl $0, (%esp)/(%rsp)
instead of mfence
even when mfence
is available, but I haven't experimented with the downsides. Using -64(%rsp)
or something might make it less likely to lengthen a data dependency on something hot (a local or a return address), but that can make tools like valgrind unhappy.
lfence
is never useful for memory ordering unless you're reading from video RAM (or some other WC weakly-ordered region) with MOVNTDQA loads.
Serializing out-of-order execution (but not the store buffer) isn't useful to stop StoreLoad reordering (the only kind that x86's strong memory model allows for normal WB (write-back) memory regions).
The real-world use-cases for lfence
are for blocking out-of-order execution of rdtsc
for timing very short blocks of code, or for Spectre mitigation by blocking speculation through a conditional or indirect branch.
See also When should I use _mm_sfence _mm_lfence and _mm_mfence (my answer and @BeeOnRope's answer) for more about why lfence
is not useful, and when to use each of the barrier instructions. (Or in mine, the C++ intrinsics when programming in C++ instead of asm).