Armv8 Memory System Study Notes

Click the card below to follow Arm Technology Academy

This article is selected from the “Arm Technology Blog” column of the Jishu Community, authored by RC. This article mainly helps to understand the Armv8 memory system.

Original link:

https://stdrc.cc/post/2021/08/23/armv8-memory-system/

Cache coherency

Cacheability

Normal memory can be set as cacheable or non-cacheable, and can be set separately for inner and outer.

Shareability

If set to non-shareable, the segment of memory is only used by a specific core; if set to inner shareable or outer shareable, it can be accessed by other observers (other cores, GPU, DMA devices). The difference between inner and outer is the range of cache coherence required, and the division of inner observers and outer observers is implementation defined.

Armv8 Memory System Study Notes

Figure 1

PoC&PoU

When cleaning or invalidating the cache, it can operate at a specific cache level, specifically, it can go to the following two “points”:

Point of coherency (PoC): Ensures that all observers that can access memory (CPU cores, DSP, DMA devices) see the same copy of a memory address at a “point”, generally the main memory.

Armv8 Memory System Study Notes

Figure 2

Point of unification (PoU): Ensures that a core’s icache, dcache, MMU (TLB) sees the same copy of a memory address at a “point”. For example, the unified L2 cache is the core’s PoU as shown in the figure below; if there is no L2 cache, it is the main memory.

Armv8 Memory System Study Notes

Figure 3

When we say “invalidate icache to PoU”, it refers to invalidating the icache so that it reads from the L2 cache (PoU) the next time it is accessed.

An application scenario for PoU is: after modifying its own code during execution, using two steps to refresh the cache, first, clean dcache to PoU, then invalidate icache to PoU.

Memory consistency

Armv8-A adopts a weak memory model, and reads and writes to normal memory may be executed out of order. The page table can be configured to non-reordering (which can be used for device memory).

Normal memory: RAM, Flash, ROM in physical memory, these memories allow access in a weak memory order to improve performance.

On a single-core single-threaded system, consecutive dependent str and ldr instructions will not be affected by weak memory order, for example:

str x0, [x2]

ldr x1, [x2]

Barriers

ISB

Flush the current PE’s pipeline, making instructions after this instruction need to be re-read from the cache or memory, and ensuring that instructions after this instruction can see the context-changing operations before this instruction, specifically including modifying ASID, TLB maintenance instructions, and modifying any system registers.

DMB

Ensures that other observers within the specified shareability domain observe data accesses before the dmb after they observe the dmb:

str x0, [x1]

dmb

str x2, [x3] // If the observer sees this line str, it must also see the first line str

At the same time, dmb also ensures that all subsequent data access instructions can see its previous dcache or unified cache maintenance operations:

dc csw, x5

ldr x0, [x1] // May not see dcache clean

dmb ish

ldr x2, [x3] // Must see dcache clean

DSB

Ensures the same memory order as dmb, but in addition to memory operations, it also ensures that any subsequent instructions can see the results of previous data accesses.

Waits for all cache, TLB, and branch prediction maintenance operations initiated by the current PE to be visible to the specified shareability domain.

Can be used to ensure data synchronization before the sev instruction.

An example:

str x0, [x1] // update a translation table entry

dsb ishst // ensure write has completed

tlbi vae1is, x2 // invalidate the TLB entry for the entry that changes

dsb ish // ensure that TLB invalidation is complete

isb // synchronize context on this processor

DMB&DSB options

dmb and dsb can specify the type of memory operations and shareability domain constraints through options:

Armv8 Memory System Study Notes

Figure 4

One-way barriers

  • Load-Acquire (LDAR): All loads and stores that are after an LDAR in program order, and that match the shareability domain of the target address, must be observed after the LDAR.

  • Store-Release (STLR): All loads and stores preceding an STLR that match the shareability domain of the target address must be observed before the STLR.

  • LDAXR

  • STLXR

Unlike the data barrier instructions, which take a qualifier to control which shareability domains see the effect of the barrier, the LDAR and STLR instructions use the attribute of the address accessed.

Armv8 Memory System Study Notes

Figure 5

C++&Rust memory order

Relaxed

Relaxed atomic operations only guarantee atomicity, without guaranteeing synchronization semantics.

//Thread 1:

r1=y.load(std::memory_order_relaxed); //A

x.store(r1, std::memory_order_relaxed); //B

//Thread 2:

r2=x.load(std::memory_order_relaxed); //C

y.store(42, std::memory_order_relaxed); //D

The above code, when compiled on Arm, uses str and ldr instructions, and may be executed out of order, possibly resulting in r1==r2==42, that is, A sees D, and C sees B.

A typical use case for relaxed ordering is simply increasing a counter, such as the reference count in std::shared_ptr, which only needs to guarantee atomicity without memory order requirements.

Release-acquire

Rel-acq atomic operations, in addition to guaranteeing atomicity, also ensure synchronization between stores using release and loads using acquire, where acquire must be able to see instructions before release, and release must not see instructions after acquire.

#include<thread>

#include<atomic>

#include<cassert>

#include<string>

std::atomic<std::string*>ptr;

int data;

void producer() {

std::string *p=new std::string(“Hello”);

data=42;

ptr.store(p, std::memory_order_release);

}

void consumer() {

std::string *p2;

while (!(p2=ptr.load(std::memory_order_acquire)))

;

assert(*p2==”Hello”); //never fires

assert(data==42); //never fires

}

int main() {

std::thread t1(producer);

std::thread t2(consumer);

t1.join(); t2.join();

}

In the above code, once the consumer successfully loads a non-null string pointer from ptr, it must see the write operation data=42.

This code will compile to use stlr and ldar on Arm, but the semantics defined by C++ are weaker than what stlr and ldar actually provide; C++ only guarantees synchronization between the two threads using release and acquire.

A typical use case for rel-acq ordering is mutex or spinlock, where memory accesses prior to releasing the lock must be visible to observers acquiring the lock simultaneously.

Release-consume

Similar to rel-acq, but does not guarantee that memory operations after consume will not complete before release; it only guarantees that instructions dependent on consume load operations will not be moved ahead of consume. That is, consume is not a critical section but only uses the results of memory operations before release.

Note that currently (2/2015) no known production compilers track dependency chains: consume operations are lifted to acquire operations.

#include<thread>

#include<atomic>

#include<cassert>

#include<string>

std::atomic<std::string*>ptr;

int data;

void producer() {

std::string *p=new std::string(“Hello”);

data=42;

ptr.store(p, std::memory_order_release);

}

void consumer() {

std::string *p2;

while (!(p2=ptr.load(std::memory_order_consume)))

;

assert(*p2==”Hello”); //never fires: *p2 carries dependency from ptr

assert(data==42); //may or may not fire: data does not carry dependency from ptr

}

int main() {

std::thread t1(producer);

std::thread t2(consumer);

t1.join(); t2.join();

}

In the above code, since assert(data==42) does not depend on the consume load instruction, it may execute before loading the non-null pointer, thus not guaranteeing visibility of the data=42 write.

Sequentially-consistent

Seq-cst ordering guarantees a similar memory order to rel-acq; if a thread’s seq-cst load sees another thread’s seq-cst store, it must see instructions before the store, and instructions after the load will not see instructions before the store. Additionally, seq-cst ensures that all seq-cst instructions seen by each thread have a consistent total order.

A typical use case is when multiple producers and multiple consumers are involved, ensuring that multiple consumers can see a consistent total order of producer operations.

#include<thread>

#include<atomic>

#include<cassert>

std::atomic<bool>x={false};

std::atomic<bool>y={false};

std::atomic<int>z={0};

void write_x() {

x.store(true, std::memory_order_seq_cst);

}

void write_y() {

y.store(true,std::memory_order_seq_cst);

}

void read_x_then_y() {

while (!x.load(std::memory_order_seq_cst))

;

if (y.load(std::memory_order_seq_cst)) {

++z;

}

}

void read_y_then_x() {

while (!y.load(std::memory_order_seq_cst))

;

if (x.load(std::memory_order_seq_cst)) {

++z;

}

}

int main() {

std::thread a(write_x);

std::thread b(write_y);

std::thread c(read_x_then_y);

std::thread d(read_y_then_x);

a.join(); b.join(); c.join(); d.join();

assert(z.load() !=0); //will never happen

}

In the above code, read_x_then_y and read_y_then_x cannot see the opposite assignment order of x and y, so at least one will execute ++z.

Seq-cst and other orderings mixed may lead to unexpected results, as in the following example, for thread 1, A sequenced before B, but for other threads, they may see B first, and then A later, resulting in C seeing B, getting r1=1, D seeing E, getting r2=3, and F not seeing A, getting r3=0.

//Thread 1:

x.store(1, std::memory_order_seq_cst); //A

y.store(1, std::memory_order_release); //B

//Thread 2:

r1=y.fetch_add(1, std::memory_order_seq_cst); //C

r2=y.load(std::memory_order_relaxed); //D

//Thread 3:

y.store(3, std::memory_order_seq_cst); //E

r3=x.load(std::memory_order_seq_cst); //F

References

https://developer.arm.com/documentation/100941/0100

https://developer.arm.com/documentation/den0024/a/Caches

https://developer.arm.com/documentation/den0024/a/Memory-Ordering

https://developer.arm.com/documentation/dui0489/c/arm-and-thumb-instructions/miscellaneous-instructions/dmb–dsb–and-isb

https://en.cppreference.com/w/cpp/atomic/memory_order

Recommended Reading

  • Arm Series – Armv8-A

  • Overview of Arm Compiler

  • Arm’s Support for Device Passthrough under Virtualization

Armv8 Memory System Study NotesArmv8 Memory System Study NotesArmv8 Memory System Study Notes

Long press to recognize the QR code to join the group chat: Arm Technology Academy Reader Group; if the group QR code is invalid, please directly add Miss Jishu’s WeChat (aijishu20) for an invitation to join the group.

Follow Arm Technology Academy

Leave a Comment