5 ms·
Porting code from SPARC to x86 is much easier than the other way around. Most x86 instructions allow unaligned reads/writes, whereas SPARC will SIGBUS on unali
by KMag 6y ago
Porting code from SPARC to x86 is much easier than the other way around. Most x86 instructions allow unaligned reads/writes, whereas SPARC will SIGBUS on unaligned access (at least on the processors from the late 1990s). The SPARC memory model is also more lax than x86, so x86 hides more concurrency bugs than x86 does.
- tinalumfoil 6y ago> The SPARC memory model is also more lax than x86, so x86 hides more concurrency bugs than x86 does. 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?
- qwertycrackers 6y agoNo, a strict concurrency model means things are more likely to be consistently sequenced; that is, you'll encounter less actual concurrency artifacts. If this model is made more lax, you might discover that you had UB which the previous model was not exploiting.
- mhh__ 6y agoYou're more likely to luck into the behaviour you want with x86 than a more relaxed memory model. I think the Alpha takes the pip for the latter, but I was only something like 3 months old when the IP was sold to Intel let alone in widespread use, so I could be wrong.
- KMag 6y agoYes, the DEC Alpha AXP was a beast of a chip family. The Alpha design team made nearly as few guarantees as possible in order to leave nearly as much room for optimization as possible. The Alpha's lax memory model provided the least-common denominator upon which the Java memory model is based. A stronger Java memory model would have forced a JVM on the Alpha to use a lot more atomic operations. 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.
- masklinn 6y ago> 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 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.
- jabl 6y agoIIRC SunOS/Solaris always used TSO, but Linux originally used RMO, however they switched to TSO once chips that only supported TSO appeared on the market.
- KMag 6y ago> Am I correct that more concurrency bugs would be hidden in a less lax architecture? 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.
- commandlinefan 6y agoI’m having trouble following - can you post a code example?
- Majromax 6y agoThis has been in the news again lately with Apple Silicon. The ARM architecture has a weaker memory model than x86 in that it does not normally provide "total store ordering". Under that memory model, if thread A executes (from an initially zeroed heap): 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_silicon_has_a_runtime_toggle_for_tso_to/ 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).
- KMag 6y ago> if (a == 0) assert(b == 0); 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).
- gpderetta 6y agoWhile the SPARC architecture allows for a very relaxed memory model, I belive all SPARC implementations have been TSO (i.e. exactly the same as x86) for a long time (and IIRC even non TSO SPARCs had mode bits to force system wide TSO).
- masklinn 6y ago> I belive all SPARC implementations have been TSO (i.e. exactly the same as x86) for a long time (and IIRC even non TSO SPARCs had mode bits to force system wide TSO). 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).
- KMag 6y ago> GP may have confused SPARC and Alpha 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.
- dekhn 6y agoOn the other hand, on Sun machines you could free() the same pointer twice, but on Linux, it would crash. This led to some small effort during early ports.
- KMag 6y agoInstead of "Sun machines", I think you mean Solaris (or SunOS). Sun made both SPARC and x86 machines. You can run Linux on x86 machines. For a bit, I ran Solaris 7 on my x86 desktop. 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.
- KMag 6y agoErr... I mean you can run Linux on both x86 and SPARC machines.
- matthias509 6y agoBy default yes, though I worked on a codebase which was ported from M68k to SPARC which had a lot of misaligned memory access. You could opt into having "packed" structs with #pragma pack and avoid the runtime error by compiling all the code with --misalign. 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.