As I recall, this had a performance penalty since memory reads/writes are now byte-by-byte. Also it could make code which assumed atomic reads/writes of 32 bit quantities go awry.
This double-free behavior was a part of the memory allocator implementation, not a feature of the processor. Linking (or LD_PRELOAD'ing) against another memory allocator could have gotten you the other behavior on either platform.
Note that even with the Solaris memory allocator not corrupting state or abort()ing if you double-freed a pointer, if a second thread in the same process had called malloc() and gotten that same block of memory in the middle of your double-free, then a later malloc() could also get a pointer to that same block of memory, and you'd unintentionally have two unrelated pointers to the same memory block, almost certainly resulting in memory corruption. One of my fist projects at my first job was fixing a bug that turned out to be memory corruption caused by a double-free. Continuing to apparently run fine despite a very serious and hard to debug bug isn't generally considered a good feature.
The internet does indicate that that seems to have been the default, though it was possible to enable more relaxed models starting from v8 (PSO) and v9 (RMO).
Most of the relaxation seems to have been dropped in the meantime:
> SPARC M7 supports only TSO, with the exception that certain ASI accesses (such as block loads and stores) may operate under RMO. […] SPARC M7 ignores the value set in this field and always operates under TSO.
GP may have confused SPARC and Alpha, the latter having (in)famously relaxed approach to memory ordering (uniquely reordering even dependent loads e.g. if you load a pointer then read the value at that pointer you may get stale data).
I seem to have confused which of the 3 memory ordering states was the default and supported on all models. I'm well aware of the Alpha.
> e.g. if you load a pointer then read the value at that pointer you may get stale data
For anyone thinking that sounds horrifying, a single core doing the writes and read won't observe anything out of the ordinary.
The designers of the Alpha thought "Okay, we can simplify and speed up the cache coherency logic if the "happens before" temporal dependency introduced by a memory fence between two writes won't make any guarantees about two reads on another core, unless there's also a "happens before" temporal dependency introduced by a memory fence between the two reads". This sounds totally reasonable, particularly at a time when lockfree data structures weren't as popular. Reads and writes done while holding a mutex work just like any other processor: the memory fence involved in acquiring a mutex guarantees that any reads made while holding the mutex will see all writes made before the previous release operation on that mutex.
The difficulty with the Alpha memory model comes mostly in writing lockfree data structures, where the safety comes from modifying some data structure while no other threads can see it, then atomically changing a pointer (usually a load-locked/store-conditional or compare-and-swap) to make that pointer available to other threads. On most architectures, you only need a memory fence right before (or a part of) the write operation that changes the pointer. On most architectures, this happens-before guarantee will be seen by all normal read operations. On Alpha, you also need a memory fence between reading that pointer and chasing it. In C, the loading the pointer and chasing it happen in a single expression *p, so you need to load the global pointer into a local variable, execute a memory fence, and then deference the pointer (or else use inline assembly, C11 atomics came out way way after the DEC Alpha AXP).
Lockfree data structures become more popular well after the Alpha was released, so it's understandable why the Alpha designers were willing to make these optimizations that don't affect code that correctly uses mutexes to protect global mutable state.
I think you mistyped this sentence. Did you mean x86 is more lax, so SPARC hides more concurrency bugs? Am I correct that more concurrency bugs would be hidden in a less lax architecture?
All processors (or at least all processors I'm aware of) will make a core observe its own writes in the order they appear in the machine code. That is, a core by itself will be unable to determine if the processor performs out-of-order memory operations. If the machine code says Core A makes writes A0 and then A1, it will always appear to Core A that A0 happened before A1. As far as I know, all processors also ensure that all processors will agree to a single globally consistent observed ordering of all atomic reads and atomic writes. (I can't imagine what atomic reads and writes would even mean if they didn't at least mean this.)
On top of the basic minimum guarantees, x86 and x86_64 (as well as some SPARC implementations, etc.) have a Total Store Ordering memory model: if Core A makes write A0 followed by A1, and Core B makes write B0 followed by B1, the two cores may disagree about whether A0 or B0 happened first, but whey will always agree that A0 happened before A1 and B0 before B1, even if none of the writes are atomic.
In a more relaxed memory model like the SPARC specification or Aarch64 specification (and I think RISC-V), if the machine code says Core A makes write A0 before A1, Core B might see A1, but not yet see A0, unless A0 was an atomic write. If Core B can see a given atomic write from Core A, it's also guaranteed that Core B can see all writes (atomic and non-atomic) that Core A thinks it made before that atomic write.
With the DEC Alpha, the hardware designers left themselves almost the maximum amount of flexibility that made any semantic sense: if Core B makes an atomic read, then that read (and any reads coming after it in machine code order) is guaranteed to see the latest atomic write from Core A, and all writes that came before that atomic write in machine code order. On the Alpha, you can think of it as all of the cores having unordered output buffers and unordered input buffers, where atomic writes flush the core's output buffer and atomic reads flush the input buffer. All other guarantees are off. (Note that even under this very lax memory model, as long as a mutex acquisition involves an atomic read and an atomic write, and a mutex release involves an atomic write, you'll still get correct behavior if you protect all reads and writes of shared mutable state with mutexes. A reader's atomic read in mutex acquisition guarantees that all reads while holding the mutex will see all writes made before another thread released the mutex.) This might be slightly wrong, but it's roughly what I remember of the Alpha memory model.
The thing that confused some programmers with the Alpha is that with most memory models, if one thread makes a ton of atomic writes, and another thread makes a ton of non-atomic reads, the reading thread will still never see the writes in a different order than what the writer thought it wrote. There's no such guarantee on Alpha.
On a side note, the Alpha team was also pretty brutal about only allowing instructions that were easy for compilers to generate and showed a performance improvement in simulations on some meaningful benchmark. The first generation of the Alphas didn't even have single-byte loads or stores and relied on compilers to perform single-byte operations by bit manipulation on 32-bit and 64-bit loads and stores.
Many of the Alpha design people went on to the AMD K6 III (first AMD chips to give Intel a run for their money in the desktop gaming market), the PASemi PWRFicient (acqui-hired by Apple to start their A-series / Apple Silicon team), AMD Ryzen, etc.)
When I bought my first computer in the fall of 1997, the fastest Intel desktop processors were 300 MHz PIIs. DEC Alphas at the time were running at 500 MHz, and had more instructions per clock, particularly in the floating point unit. The Cray T3E supercomputer used DEC Alphas for good reason.
AFAIK SPARC has always used TSO by default, and while v8 and v9 introduced relaxed memory modes (opt-in), these have been dropped from recent models e.g. M7 is back to essentially TSO-only. While it is backwards-compatible and supports the various instructions and fields, it ignores them and always runs in TSO.
No, a more strict (less lax) memory model gives the processor less freedom to re-order memory operations, meaning that missing memory fences (explicit ordering) have potentially less effect vs. more lax memory models.
The SPARC memory model makes fewer guarantees (less strict), allowing the processor more freedom in ordering memory operations, potentially getting more performance. (There's a reason aarch64 went with a more lax memory model than x86, despite being designed decades later.) The downside is that bugs with missing memory fences are more likely to show up.
a = 1; b = 1;
then thread B can safely execute: if (b == 1) assert(a == 1);
if (a == 0) assert(b == 0);
x86 provides this guarantee, but ARM does not -- thread B might see the b=1 write before the a=1 write.Apple Silicon has a runtime toggle (https://old.reddit.com/r/hardware/comments/i0mido/apple_sili...) to provide for that behaviour, which greatly improves performance of translated x86 code (i.e. the translator does not need to insert precautionary memory barriers).
Even on x86, you can't make this assertion without any synchronization primitives (mutexes, etc.). Without synchronization, the a = 1; b = 1; can run between the (a == 0) and assert(b == 0).