Memory Model: What is the difference between `store(x, relaxed); atomic::fence(seq_cst);` and `store(x, seq_cst)`

116 Views Asked by At

I see a code snippet in rust crossbeam-epoch library

pub(crate) fn pin(&self) -> Guard {
...
   self.epoch.store(new_epoch, Ordering::Relaxed);
   atomic::fence(Ordering::SeqCst);
...

I am curious that

  • why not just self.epoch.store(new_epoch, Ordering::SeqCst);?
  • Do they provide a same memory ordering guarantee?
  • Is there any performance difference between these two approach?

(Maybe) Equivalent C++ code:

epoch.store(new_epoch, std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_seq_cst);

v.s.

epoch.store(new_epoch, std::memory_order_seq_cst);
1

There are 1 best solutions below

0
Nate Eldredge On

They have different semantics for how they can be reordered with other loads and stores in this thread.

epoch.store(new_epoch, std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_seq_cst);

The seq_cst fence acts as a full barrier; no loads or stores can be reordered across it. However, nothing here prevents the store to epoch from being reordered with earlier loads or stores. (Provided that the ordering on those loads and stores also allows it; i.e. the store to epoch can be reordered with earlier relaxed loads, and all earlier stores regardless of their specified ordering.)

epoch.store(new_epoch, std::memory_order_seq_cst);

A seq_cst store is not reordered with other seq_cst loads and stores. However, for purposes of reordering with non-seq_cst accesses, it is treated as if it were simply release. It will not be reordered with earlier loads or stores, but nothing here prevents it from being reordered with later loads and stores. (Specifically, relaxed or acquire loads, or relaxed stores.)

To be concrete, consider the following code.

std::atomic<bool> a{false}, x{false}, b{false};

void v1() {
    a.store(true, std::memory_order_relaxed);
    x.store(true, std::memory_order_relaxed);
    std::atomic_thread_fence(std::memory_order_seq_cst);
    b.store(true, std::memory_order_relaxed);
}

void v2() {
    a.store(true, std::memory_order_relaxed);
    x.store(true, std::memory_order_seq_cst);
    b.store(true, std::memory_order_relaxed);
}

void reader() {
    bool bb = b.load(std::memory_order_seq_cst);
    bool xx = x.load(std::memory_order_seq_cst);
    bool aa = a.load(std::memory_order_seq_cst);
    std::cout << aa << xx << bb;
}

In v1, the store to x can be reordered with the store to a, but not with the store to b. If reader is run in another thread, it may observe aa == false && xx == true, but it must not observe xx == false && bb == true.

In v2, the reverse is true: the store to x can be reordered with the store to b, but not with the store to a. The reader function must not observe aa == false && xx == true, but it may observe xx == false && bb == true.