In Memory ordering of atomics in Rust and Go, I described how Go and Rust differ in that Rust allows choosing from a variety of memory ordering guarantees for atomic variables, while Go has just one ordering. I ended on:
And so there you go, that’s why I think this is emblematic of Rust and Go’s design choices: Rust gives you the full menu and leaves you to figure out the best for your situation, whereas Go makes the choice for you but leaves some performance optimisations on the table.
However, this isn’t the whole story! I’ve progressed further through the Rust Atomics and Locks book and have now read the chapter on how x86-64 and ARM64 handle the different memory orderings.
It turns out that on x86-64, the processor doesn’t reorder loads and stores, with the exception that a store operation can happen after a later load operation. What this means is that (from the Rust Atomics and Locks book):
On x86-64, every memory operation has acquire and release semantics, making it exactly as cheap or expensive as a relaxed operation. Everything other than stores and fences also has sequentially consistent semantics at no extra cost.
At the time of its design, the majority of Go workloads would have been
targeting x86-64 servers. And that’s probably still true today. That makes the
choice to only offer sequentially consistent (SeqCst) atomic operations feel
more pragmatic: it makes little difference to the performance of a lot of code.
The one exception is stores: x86-64 requires a more expensive instruction to
provide SeqCst operations for stores. But for load and read-modify-write
instructions, which are the majority of use-cases, SeqCst costs nothing extra
over Rust’s Relaxed semantics!
ARM64, however, does have a difference. Even here, though (again from Rust Atomics and Locks):
On ARM64, acquire and release semantics are not as cheap as relaxed operations, but do include sequentially consistent semantics at no extra cost.
So broadly speaking, Relaxed ordering will win you some speed on ARM64, but
Acquire, Release and SeqCst result in the same processor instructions.
Again, this means the Go choice isn’t so bad.
On the other hand, the Rust approach is more flexible to wringing the most out of the architectures where the behaviour does differ.
Overall, I think the statement that Rust is more flexible and Go more simple —
which reflects the philosophies of the languages — is still correct. But Go’s
choice to offer SeqCst only makes a lot more sense in light of the fact that
x86-64 basically does that every time anyway. The Go authors are not leaving
performance on the table for their primary use-case.
Read more at: Rust Atomics and Locks — Chapter 7. Understanding the Processor