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
This article articulates a lot of how I feel about using AI today. I can see that I will use it more, but I don’t know yet how to use it more while maintaining the level of understanding of the code I want to retain.
I attempt to set a high bar for what I want code to look like, and I want to understand the code I ship. Under pressure, or in a discussion with another human, I want to be able to explain what the system does without first having to ask [an ai model] to explain it to me. Now there is obviously a question if this desire to understand the code is one that I will still have a few years from now. For now I have not moved past the point of comprehension being important to me.
Given this desire, there is something I lack with my experience of code written without me paying attention, particularly from loops. Present-day models tend to produce code that is too defensive, too complex, too local in its reasoning. […] At least for my taste, present-day hands-off harnesses like Claude Code with ultracode produce worse code than what we were producing last autumn. That’s because Claude Code, with Fable for instance will be working uninterrupted on a problem for thirty minutes or more, when previously the process would have been much more human in the loop.
In a recent conversation, I realised I knew consistent hashing existed, and what you could use it for. However, I couldn’t immediately bring to mind how it actually worked. Not awful — I knew I could easily find out — but I also felt I should have known it.
I’ve found that a good way to get an algorithm to stick in my head is to implement it. Even better to then write about it. I remember the parts of toykv that I wrote about more clearly than those I only coded.
There are a few other algorithms that I like but I can’t quite remember. Rendezvous hashing and reservoir sampling come to mind as ones where embedding the idea solidly would be valuable. I’d also like a deeper understanding of bloom filters. I like data sketches, so perhaps I’ll be brave enough to look harder at hyperloglog and count-min. We’ll see how it goes.
So let’s start a small series here to get some more depth and memories of these useful algorithms. And where better to start than getting consistent hashing fixed securely into my brain.
Today I learned that you can make strongly typed CLI flags using Go’s standard
flag package. flag is a package I’m fond of and I’ve written about it
before. I think the package is
underappreciated and harbours hidden depths.
You make strongly typed flags in Go by implementing the flag.Value interface.
One thing I really liked about this approach is that it gives you a contained
place to validate flag values without polluting main!
Let’s explore the idea by creating a strongly typed flag that selects between pretty logs (for when you are developing) and JSON logs (for production).
I’ve been reading Rust Atomics and Locks by Mara Bos. If you’re into low-level concurrency primitives, it’s a great book. The rest of you can leave now 🙃
Memory ordering is a place where Rust and Go diverge, and I think it’s illustrative of the difference in language philosophy. Rust provides a selection of memory orderings per atomic variable operation, whereas Go provides just one. Rust chooses raw performance whereas Go selects ease-of-use.
Let’s dig in.