Table of Contents
Memory Model
Alpha has the weakest memory ordering model of any architecture Linux has supported. It influenced the design of the Linux kernel memory model, and is the reason READ_ONCE() is more expensive on Alpha than on other architectures.
The sections below cover the guarantees the architecture makes, the behavior of the hardware, and the code the compiler and the kernel emit. Statements are sourced from the Architecture Reference Manual, from current Linux and GCC source, or from the compiler output reproduced below.
See also Unaligned Access and Byte and Word Access.
Rules for user-space code
- Use the C11 or C++11 atomics, or GCC's
__atomicbuiltins, rather than hand-written barriers. GCC emits the barriers Alpha needs for each memory order (see What GCC emits). - Never rely on a data dependency for ordering. A load through a pointer that was just loaded may return stale data on Alpha (see Why dependent loads reorder).
memory_order_consumeis safe, because every implementation promotes it tomemory_order_acquire; code that uses a relaxed load and depends on the address dependency is not. - Keep atomic objects aligned. Atomic operations are load-locked/store-conditional loops, and the kernel never emulates a misaligned
LDx_LorSTx_C: a misaligned atomic object, for example in a packed structure, always raisesSIGBUS. See Unaligned Access. - Expect sub-word atomics to be emulated. Alpha's load-locked and store-conditional instructions operate only on longwords and quadwords. GCC implements atomic operations on 8-bit and 16-bit objects by operating on the aligned quadword that contains them, masking the other bytes. 1) They are correct but slower, and contend with any other atomic variable in the same lock range, which is at least the containing quadword (see Reservation granularity). There is no 16-byte atomic instruction, so 16-byte atomic operations are not lock-free and are handled by
libatomic, which must be linked with-latomic. 2) - Follow the load-locked/store-conditional rules in inline assembly. Only register-to-register integer instructions may appear between
LDx_LandSTx_C, and no branch may be taken on the path from one to the other; a branch that abandons the sequence without executing theSTx_Cis permitted (see Load-Locked / Store-Conditional). - Execute
CALL_PAL IMBafter writing code. A JIT compiler must make newly written instructions visible to the instruction stream;__builtin___clear_cache()emits theIMB. 3)
Permitted reorderings
| Reordering | Alpha | x86 (TSO) | ARMv7 | SPARC TSO |
|---|---|---|---|---|
| Store to store | Yes | No | Yes | No |
| Load to load | Yes | No | Yes | No |
| Load to store | Yes | No | Yes | No |
| Store to load | Yes | Yes | Yes | Yes |
| Dependent load to load | Yes | No | No | No |
The last row distinguishes Alpha from the others. Every other architecture guarantees that when a pointer is loaded and a second load is made through it, the second load observes memory at least as new as the first. Alpha does not.
Why dependent loads reorder
The kernel's Documentation/memory-barriers.txt names Alpha as the machine on which this reordering can actually be observed, and attributes it to the cache rather than to speculative execution: some Alpha implementations have a split data cache, whose two banks can be updated at different times. 4)
The two loads go to different cache banks, and the banks are updated independently. The dereferencing processor can see the new pointer from one bank while the old contents of the target are still live in another. The wording "some versions of the Alpha CPU" is significant: this is an implementation property that the architecture permits, not behavior exhibited by every Alpha.
The kernel's Alpha READ_ONCE() implementation cites the same explanation, which is given in more detail in chapter 15 of Paul McKenney's "perfbook". 5) 6)
Barrier instructions
Alpha has exactly two memory barrier instructions, plus a PALcode call for the instruction stream.
| Instruction | Effect |
|---|---|
MB | Full barrier. All previous loads and stores access memory before any subsequent load or store does, as observed by other processors. |
WMB | Write barrier. Writes before it complete before writes after it, and are not aggregated with them. Loads may cross it freely. |
CALL_PAL IMB | Instruction memory barrier, for self-modifying or newly loaded code. |
There is no read barrier instruction: the binutils opcode table lists mb and wmb and nothing else of the kind. Linux therefore defines rmb() as a full mb: 7)
#define mb() __asm__ __volatile__("mb": : :"memory") #define rmb() __asm__ __volatile__("mb": : :"memory") #define wmb() __asm__ __volatile__("wmb": : :"memory")
The Architecture Reference Manual is precise about what MB does and does not provide: 8)
Note that MB ensures serialization only; it does not necessarily accelerate the progress of memory operations.
and about what happens without WMB: 9)
In the absence of a WMB instruction, stores to memory or non-memory-like regions can be aggregated and/or buffered and completed in any order.
The dependent load problem in practice
/* Thread A, the writer */ node->value = 42; smp_wmb(); /* value store is visible before the pointer store */ list->head = node; /* Thread B, the reader. BROKEN on Alpha. */ struct node *p = list->head; int v = p->value; /* may read the pre-initialization contents */
Thread B needs a barrier between the two loads even though the second load depends on the first. The kernel once had a separate smp_read_barrier_depends() for this, an mb on Alpha and nothing elsewhere. Since Linux 4.15 Alpha's READ_ONCE() includes the barrier, 10) and Linux 5.9 removed smp_read_barrier_depends() entirely. 11) 12) Only Alpha architecture code still needs to consider the problem explicitly. 13)
What Alpha does today is a plain volatile load followed by a full mb, whenever CONFIG_SMP is set: 14)
#define __READ_ONCE(x) \
({ \
__unqual_scalar_typeof(x) __x = \
(*(volatile typeof(__x) *)(&(x))); \
mb(); \
(typeof(x))__x; \
})
Because that barrier is already present, smp_load_acquire() on Alpha is defined as __READ_ONCE(): 15)
#define __smp_load_acquire(p) \
({ \
compiletime_assert_atomic_type(*p); \
__READ_ONCE(*p); \
})
The porting consequence is that kernel code using READ_ONCE(), rcu_dereference() or smp_load_acquire() is unaffected by the dependent-load problem, as is userspace code using C11 or C++11 atomics, where every implementation promotes memory_order_consume to memory_order_acquire. Hand-written lock-free code that relies on data dependencies for ordering is incorrect on Alpha.
Load-Locked / Store-Conditional
Alpha has no atomic read-modify-write instruction. Every atomic operation is built from LDL_L/LDQ_L and STL_C/STQ_C. STx_C writes 1 into its source register on success and 0 on failure, and clears the lock flag either way.
Forward progress rules
The Architecture Reference Manual places hard restrictions on what may appear between the load-locked and the store-conditional. These are correctness requirements rather than recommendations: 16)
If any other memory access (LDx, LDQ_U, STx, STQ_U) is done on the given processor between the LDx_L and the STx_C, the sequence above may always fail on some implementations; hence, no useful program should do this. If a branch is taken between the LDx_L and the STx_C, the sequence above may always fail on some implementations; hence, no useful program should do this. (CMOVxx may be used to avoid branching.) If a subsetted instruction (for example, floating-point) is done between the LDx_L and the STx_C, the sequence above may always fail on some implementations, because of the Illegal Instruction Trap; hence, no useful program should do this. If a large number of instructions are executed between the LDx_L and the STx_C, the sequence above may always fail on some implementations, because of a timer interrupt always clearing the lock_flag before the sequence completes; hence, no useful program should do this. Hardware implementations are encouraged to lock no more than 128 bytes.
Only register-to-register integer work may appear between the two instructions.
Atomic increment
The kernel's implementation shows the idiom, including the branch layout: 17)
/* * To get proper branch prediction for the main line, we must branch * forward to code at the end of this object's .text section, then * branch back to restart the operation. */ #define ATOMIC_OP(op, asm_op) \ static __inline__ void arch_atomic_##op(int i, atomic_t * v) \ { \ unsigned long temp; \ __asm__ __volatile__( \ "1: ldl_l %0,%1\n" \ " " #asm_op " %0,%2,%0\n" \ " stl_c %0,%1\n" \ " beq %0,2f\n" \ ".subsection 2\n" \ "2: br 1b\n" \ ".previous" \ :"=&r" (temp), "=m" (v->counter) \ :"Ir" (i), "m" (v->counter)); \ } \
The retry path lives in .subsection 2 so that both in-line conditional branches are forward branches, and are therefore predicted not taken. The Architecture Reference Manual makes the same point: 18)
Both conditional branches are forward branches, so they are properly predicted not to be taken (to match the common case of no contention for the lock).
This macro generates the unordered form. A fully ordered read-modify-write requires a barrier on both sides; Linux places them outside the assembly.
Compare and swap
static inline unsigned long ____cmpxchg_u32(volatile int *m, int old, int new) { unsigned long prev, cmp; __asm__ __volatile__( "1: ldl_l %0,%5\n" " cmpeq %0,%3,%1\n" " beq %1,2f\n" " mov %4,%1\n" " stl_c %1,%2\n" " beq %1,3f\n" "2:\n" ".subsection 2\n" "3: br 1b\n" ".previous" : "=&r"(prev), "=&r"(cmp), "=m"(*m) : "r"((long) old), "r"(new), "m"(*m) : "memory"); return prev; }
with the barriers wrapped around the whole thing: 19)
/*
* The leading and the trailing memory barriers guarantee that these
* operations are fully ordered.
*/
#define arch_cmpxchg(ptr, o, n) \
({ \
__typeof__(*(ptr)) __ret; \
__typeof__(*(ptr)) _o_ = (o); \
__typeof__(*(ptr)) _n_ = (n); \
smp_mb(); \
__ret = (__typeof__(*(ptr))) ____cmpxchg((ptr), \
(unsigned long)_o_, (unsigned long)_n_, sizeof(*(ptr)));\
smp_mb(); \
__ret; \
})
Because the barriers are outside, a failing cmpxchg() is still fully ordered. Hand-written CAS loops commonly branch out of the loop on mismatch and skip the trailing barrier.
On the mismatch path the reservation is abandoned, which the architecture permits: 20)
LDx_L and STx_C instructions need not be paired. In particular, an LDx_L may be followed by a conditional branch: on the fall-through path an STx_C is done, whereas on the taken path no matching STx_C is done.
Locks
Alpha has no load-acquire or store-release instruction. Acquire and release semantics are built out of MB.
A spin-wait must not repeat the LDx_L itself. The Architecture Reference Manual: 21)
It would be a performance mistake to spin-wait by repeating the full LDQ_L..STQ_C sequence (to move the BLBS after the BEQ) because that sequence may repeatedly change the software lock_variable from "locked" to "locked," with each write causing extra access delays in all other caches that contain the lock_variable. In the extreme, spin-waits that contain writes may deadlock.
In the Linux implementation the contended spin loop at label 2 uses a plain ldl rather than ldl_l: 22)
static inline void arch_spin_lock(arch_spinlock_t * lock) { long tmp; __asm__ __volatile__( "1: ldl_l %0,%1\n" " bne %0,2f\n" " lda %0,1\n" " stl_c %0,%1\n" " beq %0,2f\n" " mb\n" ".subsection 2\n" "2: ldl %0,%1\n" " bne %0,2b\n" " br 1b\n" ".previous" : "=&r" (tmp), "=m" (lock->lock) : "m"(lock->lock) : "memory"); } static inline void arch_spin_unlock(arch_spinlock_t * lock) { mb(); lock->lock = 0; }
As of Linux 7.3, Alpha has not been converted to qspinlock; the code above is the current implementation.
On barrier placement, the Architecture Reference Manual prescribes: 23)
<acquire software lock>
MB (memory barrier #1)
<critical section -- read/write shared data>
WMB or MB (memory barrier #2)
<clear software lock>
The first memory barrier prevents any reads (from within the critical section) from being prefetched before the software lock is acquired; such prefetched reads would potentially contain stale data. The second memory barrier prevents any writes (and reads if MB is used instead of WMB) from within the critical section from being delayed past the clearing of the software lock.
The acquire-side barrier must be MB. A WMB on the release side orders only the critical section's stores before the unlock; its loads may still complete after the lock is released. WMB therefore suffices only if nothing depends on those loads completing inside the critical section. Linux uses a full mb().
Memory-Mapped I/O
Device registers are accessed through readl(), writel() and the related accessors. On Alpha these already carry the necessary barriers, and writel_relaxed is defined as plain writel: 24)
extern inline u32 readl(const volatile void __iomem *addr) { u32 ret; mb(); ret = __raw_readl(addr); mb(); return ret; } extern inline void writel(u32 b, volatile void __iomem *addr) { mb(); __raw_writel(b, addr); }
writel() has a leading barrier and no trailing one. The asymmetry is deliberate: a barrier does not make a write to a device register complete: 25)
If an Alpha AXP processor writes a location that is a control register within an I/O device, then executes a memory barrier, then writes a location in memory (in a memory-like or non-memory-like region), the I/O device may be able to detect (via read access) the result of the memory write before receiving and responding to the write of its own control register. In almost every case, a mechanism that ensures the completion of writes to control register locations within I/O devices is provided. The normal and strongly recommended mechanism is to read a location after writing it, which guarantees that the write is complete.
A device write is forced to complete by reading the register back. The kernel does this when setting the HAE register: 26)
alpha_mv.hae_cache = new_hae; *alpha_mv.hae_register = new_hae; mb(); /* Re-read to make sure it was written. */ new_hae = *alpha_mv.hae_register;
For a single processor accessing the same address, no barrier is needed at all. From the Architecture Reference Manual: 27)
A read to physical address x will always return the value written by the immediately preceding write to x in the processor issue sequence.
The exposure is ordering between different registers, and ordering as the device sees it across a bus bridge.
Instruction stream and TB invalidation
Writing instructions to memory does not make them visible to the instruction stream. The architecture requires CALL_PAL IMB whenever the instruction stream changes, and on a multiprocessor the operating system must arrange for every processor that may hold stale copies to do the same. 28) MB is not sufficient. User-space code gets the IMB from __builtin___clear_cache() (see Rules for user-space code).
Translation buffer invalidation is not an instruction either: under the OSF/1 PALcode that Linux uses, TBIA, TBIS and related operations are the PALcode call CALL_PAL tbi, with the operation selected by a register argument. 29)
What GCC emits
The output below is from alpha-unknown-linux-gnu-gcc 16.2.0 at -O2. The GP-relative address computation has been elided for readability.
_Atomic int flag; void st_rel(void) { atomic_store_explicit(&flag, 1, memory_order_release); } int ld_acq(void) { return atomic_load_explicit(&flag, memory_order_acquire); } void st_sc(void) { atomic_store_explicit(&flag, 1, memory_order_seq_cst); } void fence(void) { atomic_thread_fence(memory_order_seq_cst); }
st_rel: mb ; stl $2,flag($1) ; ret ld_acq: ldl $0,flag($1) ; mb ; ret st_sc: mb ; stl $2,flag($1) ; mb ; ret fence: mb ; ret
Two points follow from this output:
- A release store emits
MBbefore the store, notWMB. - GCC's Alpha back end has no
WMBpattern, andmbis the only barrier it emits. The single barrier pattern ingcc/config/alpha/sync.mdexpandsUNSPEC_MBto the string"mb", and no file ingcc/config/alpha/refers towmb. 30)
The legacy __sync builtins are full barriers by definition, and GCC brackets them accordingly:
faa: ; __sync_fetch_and_add(p, 1) mb $L10: ldl_l $0,0($16) addl $0,1,$1 stl_c $1,0($16) beq $1,$L10 mb ret $31,($26),1 cas: ; __sync_bool_compare_and_swap(p, old, new) mb $L12: ldl_l $1,0($16) cmpeq $1,$17,$0 beq $0,$L13 mov $18,$0 stl_c $0,0($16) beq $0,$L12 $L13: mb ret $31,($26),1
At label $L13 the trailing mb covers the failure path as well as the success path, matching the kernel's arch_cmpxchg().
Summary
| Need | On Alpha |
|---|---|
| Full barrier | MB |
| Write barrier | WMB (architecture only; GCC never emits it) |
| Read barrier | does not exist; use MB |
| Atomic read-modify-write | LDx_L/STx_C loop, with only register operates and no taken branch in between |
| Fully ordered RMW | MB before and after the loop |
| Publish a pointer, writer | WMB (or MB) before the pointer store |
| Publish a pointer, reader | MB after the pointer load; in the kernel, READ_ONCE() supplies it |
| Acquire a lock | LDx_L/STx_C loop, spin on a plain LDx, then MB |
| Release a lock | MB (WMB permitted), then the clearing store |
| MMIO | readl()/writel(); read back to force completion |
| Invalidate the TB | CALL_PAL tbi, not an instruction |
| Invalidate the I-cache | CALL_PAL IMB, not MB |
MB, WMB, LDx_L and STx_C; Chapter 5 covers the memory model, atomic update of data structures (5.5), ordering considerations for shared data structures (5.5.4) and memory-mapped I/O (5.6.4.7).
