Categories
C standard

Memory model and semantics for C

A perceptive note from Linus Torvalds about the C/C++ “memory model” is reproduced below.


From Linus Torvalds <>
Date Thu, 7 Jun 2018 08:40:49 -0700
Subject Re: LKMM litmus test for Roman Penyaev’s rcu-rr
share 0
On Thu, Jun 7, 2018 at 7:57 AM Alan Stern <stern@rowland.harvard.edu> wrote:
>
> You can look at a memory model from three points of view:
>
> 1. To a programmer, the model provides both guarantees (a certain
> code snippet will never yield a particular undesired result)
> and warnings (another snippet might yield an undesired result).
>
> 2. To a CPU designer, the model provides limits on what the
> hardware should be allowed to do (e.g., never execute a store
> before the condition of a preceding conditional branch has been
> determined — not even if the CPU knows that the store would be
> executed on both legs of the branch).
>
> 3. To a compiler writer, the model provides limits on what code
> manipulations should be allowed.
>
> Linus’s comments mostly fall under viewpoint 2 (AFAICT), and a lot of
> our thinking to date has been in viewpoint 1. But viewpoint 3 is the
> one most relevant to the code we have been discussing here.

Hmm. Yes. I’ve been ignoring it because we’ve historically just
“solved” it by having barriers (either “asm volatile” with a memory
clobber, or just a volatile access) for the cases under discussion.
There isn’t a lot of room for compiler games there. We’ve had issues
with compilers _elsewhere_, but when it comes to memory models, we’ve
always solved it with “hey, let’s make that access volatile”, or “we
will force the compiler to use *this* instruction and nothing else”.

So we’ve never actually had much worry about what a compiler could do
for these particular issues.

In fact, one of the reasons I personally doubt we will ever use the
C++ memory model things is that I simply don’t trust the stanards body
to do the right thing. The discussion about control dependencies vs
data dependencies made me 100% convinced that there is no way in hell
that the C++ standards body model can ever work reliably.

Writing the requirements down as English, and writing them down as an
“abstract machine” that had a lot of syntax caused untold and
completely unnecessary verbiage, and made the end result
incomprehensible to any normal human. Very much so to a compiler
writer.

So honestly, I’ve given up on the C++ memory model. We’ve done quite
well with inline asm and “volatile”, and since I do not believe we
will ever get rid of either, the C++ memory model isn’t much of an
improvement.

And notice that this is very much a fundamental doubt about the whole
approach that the standards language is taking. I’m not doubting that
a memory model cannot be defined. I’m doubting that it can sanely be
described with the approach that the C standards body has.

Honestly, if the C++ standards body had taken a completely different
approach of actually *definining* a virtual machine, and defining the
actual code generation (ie the language would actually *define* what
every operation did in the virtual machine), and defining what the
memory ordering within that virtual machine was, I would give it a
chance in hell of working.

But that’s not what the C++ standards body did. Instead, it tried to
define things in terms of “dependency chains” and other very
high-level abstractions.

So I do not believe for a second in the C++ memory ordering. It’s
broken. It’s broken for fundamental reasons, and I doubt we’ll ever
use it because no compiler writer will ever get it remotely right.

> Saying that a control dependency should extend beyond the end of an
> “if” statement is basically equivalent to saying that compiler writers
> are forbidden to implement certain optimizations.

Honestly, what I think the standard should do is *define* that

if (a) B else C

is defined to generate the code

load a
test
je label1;
B
jmp label2
label1:
C
label2:

in the virtual machine. And then each operation (like “load” vs
“load_acquire” vs “load_volatile”) would have well-defined semantics
in that virtual machine. And then a compiler writer would be able to
do any optimization he damn well pleased, as long as it is
semantically 100% equivalent.

That would actually give a program well-defined semantics, and I
guarantee it would make compiler writers happier too, because they’d
have a definition they can follow and think about. The memory ordering
requirements would fall out from the memory ordering defined for the
virtual machine, and then that machine could be described sanely.

In the above kind of situation, I believe we could have described what
“control vs data dependency” means, because you could have specified
it for the virtual machine, and then the compiler writers could
determine from that well-defined flow (and the kind of load they had)
whether they could turn a data dependency into a control dependency
during optimization or not.

But that’s not how the C standard is written, and it’s not how the
people involved *want* to write it. They want to write it at a high
level, not a low level.

End result: I really don’t trust what the C++ memory ordering
semantics are. It’s all much too wishy-washy, and I don’t think a
compiler writer can ever understand it.

> Now, my experience
> has been that compiler writers are loathe to give up an optimization
> unless someone can point to a specific part of the language spec and
> show that the proposed optimization would violate it.

I agree. Which is why we sometimes use the big hammer approach and
just say “don’t do this optimization at all”.

But it’s also why the language definition being unclear is so
horrible. It’s sometimes simply not all that obvious what the standard
actually allows and what it doesn’t, and memory ordering is going to
be way more fragile than even the existing confusion.

So I agree 100% with you that the LKMM cases are not likely to affect
the C standard, and not affect compiler writers.

But I also think that’s largely irrelevant, because I don’t think it’s
worth worrying about. I don’t believe we will be using the standard
memory ordering, because it’s not going to end up usable, and we’ll
continue to roll our own.

I did hope a few years ago that that wouldn’t be the case. But the
discussions I was involved with convinced me that there’s no hope.

Linus

\

\ /

Categories
programming languages

Vector optimization with C aliasing

One of the common defenses of the abuse of “undefined behavior” by C compilers, and the stupid aliasing rules in the standard is that those things are necessary for optimizations, like using vector operations.  Here’s an example of where gcc ignores the standard and does what the C language semantics require while taking advantage of Intel SIMD instructions. The C code is an example of a common technique in network code.

#include <stdlib.h>
#include <stdio.h>
#include <limits.h>
struct message { int code; int value; float data[20]; int checkval; };

 int checksum(struct message *m){
    int i, *p = (int *)m;
    int lim = sizeof(struct message) / sizeof i;
    int c = 0;
    for (i=0 ; i < lim; i++) {   c ^= *(p+i); }
  return c;

}

int main (int argc, char **argv)
{

    struct message m;
    int v;
    fread((char *)&m, sizeof(char), sizeof(m), stdin);
    v = checksum(&m);
    printf("Checksum was %d\n",v);

}
The translation.
checksum:
movdquxmm0, XMMWORDPTR [rdi+32]
movdquxmm2, XMMWORDPTR [rdi+48]
movdquxmm1, XMMWORDPTR [rdi]
movdquxmm3, XMMWORDPTR [rdi+16]
pxorxmm0, xmm2
movdquxmm4, XMMWORDPTR [rdi+64]
moveax, DWORDPTR [rdi+80]
pxorxmm1, xmm3
xoreax, DWORDPTR [rdi+84]
xoreax, DWORDPTR [rdi+88]
pxorxmm0, xmm1
movedx, eax
pxorxmm0, xmm4
movdqaxmm1, xmm0
psrldqxmm1, 8
pxorxmm0, xmm1
movdqaxmm1, xmm0
psrldqxmm1, 4
pxorxmm0, xmm1
movdeax, xmm0
xoreax, edx
ret
See the godbolt page
Categories
operating systems rtlinux

Microkernels and why academic OS research is boring

Andy Tanenbaum writes a defense of microkernels that (1) misses the content of Linus Torvald’s critique, (2) ignores the most relevant paper on software development, David Parnas’ Software Jewels paper, and (3) pretends RTLinux does not exist. The problem with microkernels is that they are not modular and the problem with all the “nearly ready for prime time” microkernels is that they are not real products and the problem with many new academic OS projects is that they don’t have much new in them.

Parnas asks why we don’t see more elegant, simple, kludge-free “jewels” in systems software and he, rather gently, explains the compatibility, time pressures, and other constraints that make “clean and lean” so elusive. Parnas is just too nice to hammer the point home. Anyone can write small fast clean operating system that doesn’t do anything useful. Over and over, researchers stumble on the discovery that as you add minor inessential feature after minor inessential feature (all in response to some importuning victim who actually needs to do something with your creation), you recreate the “bloat” that wasn’t needed when your software didn’t do anything. In fact, the natural result of making a nice clean simple lean fast OS into something useful is the creation of another implementation of POSIX. VMS and then Windows NT are very POSIX like, despite the intentions of the designers and so is Mach in OS-X and so are many traditional RTOSs. After all this time, this should not be so surprising anymore.

Linus Torvald’s response to Tanenbaum is pretty clear, but Tanenbaum misses the point:

Linus also made the point that shared data structures are a good idea. Here we disagree. If you ever took a course on operating systems, you no doubt remember how much time in the course and space in the textbook was devoted to mutual exclusion and synchronization of cooperating processes. When two or more processes can access the same data structures, you have to be very, very careful not to hang yourself. It is exceedingly hard to get this right, even with semaphores, monitors, mutexes, and all that good stuff.

My view is that you want to avoid shared data structures as much as possible. Systems should be composed of smallish modules that completely hide their internal data structures from everyone else. They should have well-defined ‘thin’ interfaces that other modules can call to get work done. That’s what object-oriented programming is all about–hiding information–not sharing it. I think that hiding information (a la Dave Parnas) is a good idea.

And this gets both Torvalds and Parnas wrong. Information hiding is only good design when the hidden information is not needed by the software it is hidden from! If you hide information that you need to share you’re just wasting time. A great example of real modularity is the splitting off of the command interpreter (first in CTSS (corrected)) from the kernel. This split is possible because the designers recognized that there is very little information exchange between kernel and interpreter. An example of a fake module is the attempt of most microkernels to split virtual memory paging and storage caching. The most elegant and efficient message passing interface that can be imagined cannot fix the problem that too much information must be shared to make the resulting monstrosity run properly. Here’s Torvalds

The fundamental result of access space separation is that you can’t share data structures. That means that you can’t share locking, it means that you must copy any shared data, and that in turn means that you have a much harder time handling coherency. All your algorithms basically end up being distributed algorithms.

And Tanenbaum’s response is that sharing data structures is hard! Well, yeah – many engineering systems are complicated and hard to do right. During the 30 years of development of CTSS, Multics, UNIX, Plan9, and Linux, many components were split off into modules. What remains cannot be broken into decoupled parts in any obvious way. Let no man or woman split what Dennis and Ken have wrought without a damn good reason. Until the hardware changes or someone discovers something new about software structure in operating systems, complaining that this kernel model is complicated seems as useful as complaining that petroleum refineries are too big and filled with chemicals. The POSIX model is a high performing engineering design – it works well and it is just perverse to assume its success is due only to the ineptness of everyone else.

It would be very interesting to study a production operating system like Linux or Windows XP and try to discover hidden modularity – functions that appear to not need to be bound to each other. Do such functions exist? Perhaps – one was discovered only a decade ago. The basis of RTLinux is the recognition that “real-time” can be separated from a time-sharing kernel and put into a module that can operate in a decoupled manner. The result was immediately useful but RTCore, the RTLinux real-time kernel, does not correspond to any of the traditional “modules” advocated by microkernelistas. There is no reason to suppose more such unconventional modules cannot be found. If we keep going back to the same boring list of “modules” that are familiar to generations of students forced to look at that ridiculous layer cake picture of an operating system, however, we’ll keep getting the same lack of results and the same “almost as fast as, almost complete” projects.

Note: RTLinux has been property of WindRiver Systems since 2007

Note2: Here’s a more abstract look at modularity.

Note 3: 2019 – I should have also said something about loadable kernel modules.