In addition to being slow for e.g. text processing, you couldn't implement a modern memory model like Java's or C++ on it, without serious compromises, due to the inability to make non-interfering writes of adjacent elements.
In addition to being slow for e.g. text processing, you couldn't implement a modern memory model like Java's or C++ on it, without serious compromises, due to the inability to make non-interfering writes of adjacent elements.
But consider that the modern model accounts for 64-byte (aka: 512-bit) cachelines. Any reads or writes to a 64-byte cacheline is going to be false-shared between cores. From a performance point of view, you cannot make non-interfering writes of adjacent elements in the modern memory model!
If you're dealing with single-threaded code, then bit-mask instructions for registers should be sufficient to emulate the ability. I can see that bit-masking every character instruction would be slower, but it would certainly get the job done.
First you have to be able to implement the model correctly. That means non-interfering writes. 99.99% of those writes will never involve any sharing (false or otherwise) and will perform just fine.
Not being able to do byte-granular writes means you simply can't implement things like this at all unless you:
1) Decide that "char" will be 32 bits (i.e., make a byte on your system 4x larger than usual).
2) Do some locking around each narrow write (or perhaps a series of them) and ensure everyone does the same.
So I am not really following your argument with regard to false sharing.
I don't know what you mean by "the modern model accounts for 64-byte (aka: 512-bit) cachelines". I don't know of any (software) memory model that even mentions cache lines. Hardware memory models don't lean heavily on them either, although they do get mentioned (e.g., on x86, Intel provides additional guarantees for some misaligned reads that do not cross a cache line, and AMD provides different guarantees).
Or 3) Decide that for performance reasons, reads/writes to an array (even a char* array or string) would be locked in 64-byte chunks at a minimum.
My point with false-sharing is that "true" byte-granularity comes with severe performance penalties on modern x86 systems. Its a thing that you want to avoid.
If one thread is reading / writing to char myString[0], while a 2nd thread is reading/writing to myString[63], your memory will ping-pong between the two threads (because x86 shares 64-bytes at a time between caches).
--------
I guess your point is that the CPU will "do the right thing, even if they're forced to do it slowly" in this case. So I guess you're right about that. Your code CAN be written as multithreaded + byte granular, its just going to be slow in practice.
I guess my point is that the high-performance programmer needs to understand this situation anyway, and that high-performance programmers have developed methodologies to avoid false-sharing. Any high-performance data-structure needs to understand false-sharing. You really shouldn't be doing things like reading/writing to myString[0] and myString[63] in two separate threads.
First note that this doesn't have anything to do with arrays specifically: any type of adjacent elements (in a struct, loose on the stack, whatever) have this problem.
That said, I have no idea how you would implement this suggestion.
E.g., how would you compile the following very simple function?
void write1(char *c) {
*c = 1;
}
> My point with false-sharing is that "true" byte-granularity comes with severe performance penalties on modern x86 systems. Its a thing that you want to avoid.It obviously does not! x86 has true byte granularity and loads and stores are very fast, just as fast as any other size. Just because the possibility of false sharing exists, doesn't mean it somehow slows down byte writes. False sharing doesn't even have anything to do with byte writes: it affects any size of access. So I am really not following your argument.
To summarize my claim:
- Modern memory models generally require that writing some "object" (which could be a primitive value like a char, int, whatever) not interfere with adjacent objects.
- This applies to all writes, unless perhaps the compiler can prove the data never escaped and cannot be shared, so in practice it means that you can only efficiently write at the smallest granularity supported by hardware.
- This means that you pretty much need 32-bit char on platforms where the smallest hardware write is 32 bits, or just give up on supporting the memory model.
None of that has anything to do with false sharing really. Not even really performance: we are just trying to get it to be correct. Saying "well you don't want to concurrently read nearby elements anyways" doesn't mean much because it doesn't lead to a practical way to implement the language (unless I am missing something).
> I guess your point is that the CPU will "do the right thing, even if they're forced to do it slowly" in this case. So I guess you're right about that. Your code CAN be written as multithreaded + byte granular, its just going to be slow in practice.
Not at all. My point has nothing to do with false sharing. You brought up false sharing.
My point is that the memory model is defined not prohibit behavior X, and platforms that need to do a large-word RMW to modify smaller elements can't effectively prohibit X.
This is true if you have false sharing or no false sharing or a small amount of false sharing. This is true even if you don't start any threads. This is true also for scenarios of "true sharing", e.g., where the other thread wants to share data (i.e., after reading the adjacent element maybe it is going to read the other one too).
> I guess my point is that the high-performance programmer needs to understand this situation anyway
It has nothing to do with the (application) programmer or performance. It is more fundamental than that: how does the compiler writer write a compiler that produces correct code under the memory model?
Structs are usually 64-bit aligned in x86 though. You need #pragma pack to be no longer 64-bit aligned on modern compilers.
> E.g., how would you compile the following very simple function?
I don't know DEC Alpha assembly, but by my understanding:
mov R1, [c] ; 32-bit move into R1
and R1, 0xFFFFFF00
or R1, 0x00000001 ; Move 1 into the first byte of R1
mov [c], R1
Later DEC Alphas had byte-wise granularity in registers. But when it was 32-bit only, you'd have to do something along these lines.If "c" was {4, 4, 4, 4}, after the store, it would be {1, 4, 4, 4}. If you are asking me where to place the memory barriers... note that C++ is Relaxed memory model and has undefined behavior unless you put the appropriate atomics<> in the correct location (which would generate a "mb" instruction for DEC Alpha).
I'm pretty sure DEC Alpha can implement C++11's memory model, albeit inefficiently with full-barriers... but it probably would work.
Totally false (just try it).
> I'm pretty sure DEC Alpha can implement C++11's memory model, albeit inefficiently with full-barriers... but it probably would work.
No, they can't (without widening char to 32 bits). The code you have shown modifies the adjacent elements. Anyone that write a nearby byte in the window between the load and the store will have their write silently clobbered.
You can't fix it with barriers. You need a lock or LL/SC loop.
The "int a" is definitely 32-bit aligned.
So you agree now that structures are not "usually 64-bit aligned on x86"?
ints will always be (at least) 32-bit aligned.
The rules are simple [1]:
1-byte primitives (e.g., chars) are 1-byte aligned
2-byte primitives (e.g., shorts) are 2-byte aligned
4-byte primitives (e.g., ints) are 4-byte aligned
8-byte primitives are 8-byte aligned
Etc.The rules for structures are also simple: they will have the alignment of their highest alignment member. That makes sense: the language rules (and ABI) are designed to make sure that primitives have (at least) the above alignment, regardless of where they are: putting them into a structure (or array, etc) can't violate that.
This is basically common across any modern platform too - not x86 specific, although some details may vary (e.g., size of primitives). There are weird cases too, like on some 32-bit platforms even (some) 64-bit values are only 32-bit aligned, and the whole 80-bit long double thing, etc: but the basic rules hold.
---
[1] Here I give some examples of data types that _commonly_ have the given size, but except for char this is not always true (e.g., some platforms have 8 byte ints, and some have 4 byte longs).
Agreed on this point.
> No, they can't (without widening char to 32 bits). The code you have shown modifies the adjacent elements. Anyone that write a nearby byte in the window between the load and the store will have their write silently clobbered.
I guess my misunderstanding was that C++11 gave this guarantee. That's pretty cool.
I guess I typically expected specific bitwise operators (ex: atomic_and or atomic_or) to be necessary in these cases. But if DEC Alpha is really the only processor in (somewhat) recent memory that doesn't allow 8-bit granularity, then it makes sense to just standardize upon 8-bit granular reads/writes.
Maybe you could try to formalize the idea of "keep private data away from data on other threads" or something, but this seems very difficult (in practice, private data is private as an emergent property of the whole application, so is not easily identifiable), and you'd have to give up on a lot of things the existing model offers in terms of write-independence.
1) doing the equivalent of CAS (or LL/SC in this case) for every single access less than 32 bits completely destroying performance.
2) declaring that a char (and a byte) is 32 bit sized, which implies no posix.
It was very bad. This was fixed in subsequent alphas though.
I knew POSIX required a lot of other things that are unspecified in C/C++ but usually have a single sane implementation, but not that particular one. I know I have static_assert(CHAR_BIT == 8, ...) sprinkled around in quite a few places.
One day I'm going to write a header that just checks a big list of things that are "obviously sane" but not actually specified in C/C++, and error out if any are not true. Then I can include this in every project and just code in this new sane world. Not strictly portable, but "portable to where it matters".
With the C++ memory model you can make std::atomic<char> larger than char. A lot of code assumes that this is not the case and that it is possible to cast between the two, but it's (surprise) undefined behavior.
If those writes are implemented by reading 4 bytes, and/oring in the byte to write and then writing 4 bytes back, they will interfere.