C as used/implemented in practice: analysis of responses

As part of a project to clarify what behaviour of C implementations is
actually relied upon in modern practice, and what behaviour is
guaranteed by current mainstream implementations, we recently
distributed a survey of 15 questions about C, https://goo.gl/AZXH3S.

We were asking what C is in current mainstream practice: the behaviour
that programmers assume they can rely on, the behaviour provided by
mainstream compilers, and the idioms used in existing code, especially
systems code. We were *not* asking what the ISO C standard permits,
which is often more restrictive, or about obsolete or obscure hardware
or compilers. We focussed on the behaviour of memory and pointers.

We've had around 300 responses, including many compiler and OS
developers, and the results are summarised below, or on the web at
http://www.cl.cam.ac.uk/~pes20/cerberus (which also has more details).
For many questions the outcome seems clear, but for some, especially
1, 2, 9, 10, and 11, major open questions about current compiler
behaviour remain; we'd greatly appreciate informed comments on those
from the relevant compiler developers (or other experts).

If you can answer these, please reply either below or by mailing the
Cerberus mailing list:

  cl-cerberus@lists.cam.ac.uk

  https://lists.cam.ac.uk/mailman/listinfo/cl-cerberus

many thanks,
Kayvan Memarian and Peter Sewell (University of Cambridge)

What is C in practice? (Cerberus survey): Conclusions

All of these seem to fall into the pattern of “The compiler is required to do what you expect, as long as it can’t prove X about your program”. That is, the only reasonable compilation in the absence of inferring some extra piece of information about your program, is the one you expect. For example, the only way to codegen a comparison between two random pointers has the meaning you expect (on common computer architectures); but if the compiler can figure something out that tells it that comparing those two pointers is undefined by the language standard, then, well, technically it can do whatever it wants.

Many people interpret this as the compiler being somewhat malevolent, but there’s another interpretation in some cases.

I have not looked in depth at the history in all the undefined behaviors mentioned in the survey, but some of the undefined behaviors are there because at some point in time the underlying system diversity made it difficult or impossible to assign a meaning. So long as the diversity that led to the desire to leave something undefined still exists, programs that use those constructs with certain expectations will fail to behave as “expected” on those targets (on a system where pointers are represented differently, your program may actually format your hard disk if you do so-and-so!).

To put it another way, what is “expected” is actually dependent on the C programmer’s knowledge of the underlying system (computer architecture, system architecture, etc.), and there will always be tension so long as the programmer is not thinking about what the C language guarantees, but rather (roughly speaking) how they would translate their code to assembly language for the system or systems that they happen to know they’re targeting. An x86 programmer doesn’t expect unaligned loads to invoke nasal demons, but a SPARC programmer does.

So if you unravel the thread of logic back through the undefined behaviors made undefined for this reason, many of these cases of exploiting undefined behavior are really an extension, on the compiler’s part, of the logic “there are some systems for which your code would invoke nasal demons, so I might as well assume that it will invoke nasal demons on this system (since the language standard doesn’t say anything about specific systems)”. Or to put it another way, the compiler is effectively assuming that your code is written to target all the systems taken into account by the C standard, and if it would invoke nasal demons on any one of them then the compiler is allowed to invoke nasal demons on all of them.

This is obviously sort of a twisted logic, and I think that a lot of the “malevolence” attributed to compilers is due to this. It certainly removes many target-dependent checks from the mid-level optimizer though.

– Sean Silva

All of these seem to fall into the pattern of "The compiler is required to
do what you expect, as long as it can't prove X about your program". That
is, the only reasonable compilation in the absence of inferring some extra
piece of information about your program, is the one you expect. For example,
the only way to codegen a comparison between two random pointers has the
meaning you expect (on common computer architectures); but if the compiler
can figure something out that tells it that comparing those two pointers is
undefined by the language standard, then, well, technically it can do
whatever it wants.

Many people interpret this as the compiler being somewhat malevolent, but
there's another interpretation in some cases.

I have not looked in depth at the history in all the undefined behaviors
mentioned in the survey, but some of the undefined behaviors are there
because at some point in time the underlying system diversity made it
difficult or impossible to assign a meaning. So long as the diversity that
led to the desire to leave something undefined still exists, programs that
use those constructs with certain expectations *will* fail to behave as
"expected" on those targets (on a system where pointers are represented
differently, your program *may* actually format your hard disk if you do
so-and-so!).

To put it another way, what is "expected" is actually dependent on the C
programmer's knowledge of the underlying system (computer architecture,
system architecture, etc.), and there will always be tension so long as the
programmer is not thinking about what the C language guarantees, but rather
(roughly speaking) how *they* would translate their code to assembly
language for the system or systems that they happen to know they're
targeting. An x86 programmer doesn't expect unaligned loads to invoke nasal
demons, but a SPARC programmer does.

So if you unravel the thread of logic back through the undefined behaviors
made undefined for this reason, many of these cases of exploiting undefined
behavior are really an extension, on the compiler's part, of the logic
"there are some systems for which your code would invoke nasal demons, so I
might as well assume that it will invoke nasal demons on this system (since
the language standard doesn't say anything about specific systems)". Or to
put it another way, the compiler is effectively assuming that your code is
written to target all the systems taken into account by the C standard, and
if it would invoke nasal demons on any one of them then the compiler is
allowed to invoke nasal demons on all of them.

Sure. However, we think we have to take seriously the fact that a
large body of critical code out there is *not* written to target what
the C standard is now, and it is very unlikely to be rewritten to do
so.

At the end of the day, code is not written purely by "thinking about
what the C language guarantees", but rather by test-and-debug cycles
that test the code against the behaviour of particular C
implementations. The ISO C standard is a very loose specification,
and we do not have good tools for testing code against all the
behaviour it permits, so that basic development technique does not -
almost, cannot - result in code that is robust against compilers that
sometimes exploit a wide range of that behaviour.

It's also the case that some of the looseness of ISO C relates to
platforms that are no longer relevant, or at least no longer
prevalent. We can at least identify C dialects that provide stronger
guarantees for the rest.

thanks,
Peter

All of these seem to fall into the pattern of "The compiler is required to
do what you expect, as long as it can't prove X about your program". That
is, the only reasonable compilation in the absence of inferring some extra
piece of information about your program, is the one you expect. For example,
the only way to codegen a comparison between two random pointers has the
meaning you expect (on common computer architectures); but if the compiler
can figure something out that tells it that comparing those two pointers is
undefined by the language standard, then, well, technically it can do
whatever it wants.

Many people interpret this as the compiler being somewhat malevolent, but
there's another interpretation in some cases.

I have not looked in depth at the history in all the undefined behaviors
mentioned in the survey, but some of the undefined behaviors are there
because at some point in time the underlying system diversity made it
difficult or impossible to assign a meaning. So long as the diversity that
led to the desire to leave something undefined still exists, programs that
use those constructs with certain expectations *will* fail to behave as
"expected" on those targets (on a system where pointers are represented
differently, your program *may* actually format your hard disk if you do
so-and-so!).

To put it another way, what is "expected" is actually dependent on the C
programmer's knowledge of the underlying system (computer architecture,
system architecture, etc.), and there will always be tension so long as the
programmer is not thinking about what the C language guarantees, but rather
(roughly speaking) how *they* would translate their code to assembly
language for the system or systems that they happen to know they're
targeting. An x86 programmer doesn't expect unaligned loads to invoke nasal
demons, but a SPARC programmer does.

So if you unravel the thread of logic back through the undefined behaviors
made undefined for this reason, many of these cases of exploiting undefined
behavior are really an extension, on the compiler's part, of the logic
"there are some systems for which your code would invoke nasal demons, so I
might as well assume that it will invoke nasal demons on this system (since
the language standard doesn't say anything about specific systems)". Or to
put it another way, the compiler is effectively assuming that your code is
written to target all the systems taken into account by the C standard, and
if it would invoke nasal demons on any one of them then the compiler is
allowed to invoke nasal demons on all of them.

Sure. However, we think we have to take seriously the fact that a
large body of critical code out there is *not* written to target what
the C standard is now, and it is very unlikely to be rewritten to do
so.

In case you're not aware of it, here's a fairly relevant blog series on
the topic of undefined behaviour in C:

http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html
http://blog.llvm.org/2011/05/what-every-c-programmer-should-know_14.html
http://blog.llvm.org/2011/05/what-every-c-programmer-should-know_21.html

At the end of the day, code is not written purely by "thinking about
what the C language guarantees", but rather by test-and-debug cycles
that test the code against the behaviour of particular C
implementations. The ISO C standard is a very loose specification,
and we do not have good tools for testing code against all the
behaviour it permits,

*cough* -fsanitize=undefined *cough*

http://clang.llvm.org/docs/UsersManual.html#controlling-code-generation

All of these seem to fall into the pattern of "The compiler is required to
do what you expect, as long as it can't prove X about your program". That
is, the only reasonable compilation in the absence of inferring some extra
piece of information about your program, is the one you expect. For example,
the only way to codegen a comparison between two random pointers has the
meaning you expect (on common computer architectures); but if the compiler
can figure something out that tells it that comparing those two pointers is
undefined by the language standard, then, well, technically it can do
whatever it wants.

Many people interpret this as the compiler being somewhat malevolent, but
there's another interpretation in some cases.

I have not looked in depth at the history in all the undefined behaviors
mentioned in the survey, but some of the undefined behaviors are there
because at some point in time the underlying system diversity made it
difficult or impossible to assign a meaning. So long as the diversity that
led to the desire to leave something undefined still exists, programs that
use those constructs with certain expectations *will* fail to behave as
"expected" on those targets (on a system where pointers are represented
differently, your program *may* actually format your hard disk if you do
so-and-so!).

To put it another way, what is "expected" is actually dependent on the C
programmer's knowledge of the underlying system (computer architecture,
system architecture, etc.), and there will always be tension so long as the
programmer is not thinking about what the C language guarantees, but rather
(roughly speaking) how *they* would translate their code to assembly
language for the system or systems that they happen to know they're
targeting. An x86 programmer doesn't expect unaligned loads to invoke nasal
demons, but a SPARC programmer does.

So if you unravel the thread of logic back through the undefined behaviors
made undefined for this reason, many of these cases of exploiting undefined
behavior are really an extension, on the compiler's part, of the logic
"there are some systems for which your code would invoke nasal demons, so I
might as well assume that it will invoke nasal demons on this system (since
the language standard doesn't say anything about specific systems)". Or to
put it another way, the compiler is effectively assuming that your code is
written to target all the systems taken into account by the C standard, and
if it would invoke nasal demons on any one of them then the compiler is
allowed to invoke nasal demons on all of them.

Sure. However, we think we have to take seriously the fact that a
large body of critical code out there is *not* written to target what
the C standard is now, and it is very unlikely to be rewritten to do
so.

In case you're not aware of it, here's a fairly relevant blog series on
the topic of undefined behaviour in C:

http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html
http://blog.llvm.org/2011/05/what-every-c-programmer-should-know_14.html
http://blog.llvm.org/2011/05/what-every-c-programmer-should-know_21.html

We're aware of those, thanks.

At the end of the day, code is not written purely by "thinking about
what the C language guarantees", but rather by test-and-debug cycles
that test the code against the behaviour of particular C
implementations. The ISO C standard is a very loose specification,
and we do not have good tools for testing code against all the
behaviour it permits,

*cough* -fsanitize=undefined *cough*

That (and other such tools) is surely a *lot* better than what we had
before, no doubt about that. And its developers and those who use it
heavily should be in a good position to comment on our survey
questions, as they are up against the same basic problem, of
reconciling what existing C code actually does vs what compilers
assume about it, to detect errors without too many false positives.
We had quite a few survey responses saying something like "sanitisers
have to allow XYZ, despite the ISO standard, because code really does
it"; in a sense, what we're doing is trying to clearly and precisely
characterise all those cases. If you or others can help with that,
please do!

But such tools are, useful and impressive though they are, aren't
really testing code against all the behaviour ISO permits - as I
understand it, they are essentially checking properties of single
(instrumented) executions, while ISO is a very loose spec, e.g. when
it comes to evaluation order choices and implementation-defined
quantities, permitting many (potentially quite different) executions
for the same source and inputs. Running with -fsanitize=undefined
will detect problems just on the executions that the current compiler
implementation happens to generate. Of course, checking against all
allowed executions of a very loose spec quickly becomes
combinatorially infeasible, so this isn't unreasonable, but at lease
we'd like to have that gold standard precisely defined, and to be able
to pseudorandomly check against it.

thanks,
Peter

>
>>
>>> All of these seem to fall into the pattern of "The compiler is
required to
>>> do what you expect, as long as it can't prove X about your program".
That
>>> is, the only reasonable compilation in the absence of inferring some
extra
>>> piece of information about your program, is the one you expect. For
example,
>>> the only way to codegen a comparison between two random pointers has
the
>>> meaning you expect (on common computer architectures); but if the
compiler
>>> can figure something out that tells it that comparing those two
pointers is
>>> undefined by the language standard, then, well, technically it can do
>>> whatever it wants.
>>>
>>> Many people interpret this as the compiler being somewhat malevolent,
but
>>> there's another interpretation in some cases.
>>>
>>> I have not looked in depth at the history in all the undefined
behaviors
>>> mentioned in the survey, but some of the undefined behaviors are there
>>> because at some point in time the underlying system diversity made it
>>> difficult or impossible to assign a meaning. So long as the diversity
that
>>> led to the desire to leave something undefined still exists, programs
that
>>> use those constructs with certain expectations *will* fail to behave as
>>> "expected" on those targets (on a system where pointers are represented
>>> differently, your program *may* actually format your hard disk if you
do
>>> so-and-so!).
>>>
>>> To put it another way, what is "expected" is actually dependent on the
C
>>> programmer's knowledge of the underlying system (computer architecture,
>>> system architecture, etc.), and there will always be tension so long
as the
>>> programmer is not thinking about what the C language guarantees, but
rather
>>> (roughly speaking) how *they* would translate their code to assembly
>>> language for the system or systems that they happen to know they're
>>> targeting. An x86 programmer doesn't expect unaligned loads to invoke
nasal
>>> demons, but a SPARC programmer does.
>>>
>>> So if you unravel the thread of logic back through the undefined
behaviors
>>> made undefined for this reason, many of these cases of exploiting
undefined
>>> behavior are really an extension, on the compiler's part, of the logic
>>> "there are some systems for which your code would invoke nasal demons,
so I
>>> might as well assume that it will invoke nasal demons on this system
(since
>>> the language standard doesn't say anything about specific systems)".
Or to
>>> put it another way, the compiler is effectively assuming that your
code is
>>> written to target all the systems taken into account by the C
standard, and
>>> if it would invoke nasal demons on any one of them then the compiler is
>>> allowed to invoke nasal demons on all of them.
>>
>> Sure. However, we think we have to take seriously the fact that a
>> large body of critical code out there is *not* written to target what
>> the C standard is now, and it is very unlikely to be rewritten to do
>> so.
>
> In case you're not aware of it, here's a fairly relevant blog series on
> the topic of undefined behaviour in C:
>
> http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html
> http://blog.llvm.org/2011/05/what-every-c-programmer-should-know_14.html
> http://blog.llvm.org/2011/05/what-every-c-programmer-should-know_21.html

We're aware of those, thanks.

>>
>> At the end of the day, code is not written purely by "thinking about
>> what the C language guarantees", but rather by test-and-debug cycles
>> that test the code against the behaviour of particular C
>> implementations. The ISO C standard is a very loose specification,
>> and we do not have good tools for testing code against all the
>> behaviour it permits,
>
> *cough* -fsanitize=undefined *cough*

That (and other such tools) is surely a *lot* better than what we had
before, no doubt about that. And its developers and those who use it
heavily should be in a good position to comment on our survey
questions, as they are up against the same basic problem, of
reconciling what existing C code actually does vs what compilers
assume about it, to detect errors without too many false positives.
We had quite a few survey responses saying something like "sanitisers
have to allow XYZ, despite the ISO standard, because code really does
it"; in a sense, what we're doing is trying to clearly and precisely
characterise all those cases. If you or others can help with that,
please do!

But such tools are, useful and impressive though they are, aren't
really testing code against all the behaviour ISO permits - as I
understand it, they are essentially checking properties of single
(instrumented) executions, while ISO is a very loose spec, e.g. when
it comes to evaluation order choices and implementation-defined
quantities, permitting many (potentially quite different) executions
for the same source and inputs. Running with -fsanitize=undefined
will detect problems just on the executions that the current compiler
implementation happens to generate. Of course, checking against all
allowed executions of a very loose spec quickly becomes
combinatorially infeasible,

Actually, I would be very, very happy to have an O(2^N) (or worse)
algorithm for checking all allowed executions :wink:

(the problem is actually undecidable; not just "infeasible")

so this isn't unreasonable, but at lease
we'd like to have that gold standard precisely defined, and to be able
to pseudorandomly check against it.

It sounds like what you are wanting is to basically make a list of
undefined behaviors in the standard and find out which ones, in practice,
should be demoted to unspecified or implementation-defined?

-- Sean Silva

>
>>
>>> All of these seem to fall into the pattern of "The compiler is
>>> required to
>>> do what you expect, as long as it can't prove X about your program".
>>> That
>>> is, the only reasonable compilation in the absence of inferring some
>>> extra
>>> piece of information about your program, is the one you expect. For
>>> example,
>>> the only way to codegen a comparison between two random pointers has
>>> the
>>> meaning you expect (on common computer architectures); but if the
>>> compiler
>>> can figure something out that tells it that comparing those two
>>> pointers is
>>> undefined by the language standard, then, well, technically it can do
>>> whatever it wants.
>>>
>>> Many people interpret this as the compiler being somewhat malevolent,
>>> but
>>> there's another interpretation in some cases.
>>>
>>> I have not looked in depth at the history in all the undefined
>>> behaviors
>>> mentioned in the survey, but some of the undefined behaviors are there
>>> because at some point in time the underlying system diversity made it
>>> difficult or impossible to assign a meaning. So long as the diversity
>>> that
>>> led to the desire to leave something undefined still exists, programs
>>> that
>>> use those constructs with certain expectations *will* fail to behave
>>> as
>>> "expected" on those targets (on a system where pointers are
>>> represented
>>> differently, your program *may* actually format your hard disk if you
>>> do
>>> so-and-so!).
>>>
>>> To put it another way, what is "expected" is actually dependent on the
>>> C
>>> programmer's knowledge of the underlying system (computer
>>> architecture,
>>> system architecture, etc.), and there will always be tension so long
>>> as the
>>> programmer is not thinking about what the C language guarantees, but
>>> rather
>>> (roughly speaking) how *they* would translate their code to assembly
>>> language for the system or systems that they happen to know they're
>>> targeting. An x86 programmer doesn't expect unaligned loads to invoke
>>> nasal
>>> demons, but a SPARC programmer does.
>>>
>>> So if you unravel the thread of logic back through the undefined
>>> behaviors
>>> made undefined for this reason, many of these cases of exploiting
>>> undefined
>>> behavior are really an extension, on the compiler's part, of the logic
>>> "there are some systems for which your code would invoke nasal demons,
>>> so I
>>> might as well assume that it will invoke nasal demons on this system
>>> (since
>>> the language standard doesn't say anything about specific systems)".
>>> Or to
>>> put it another way, the compiler is effectively assuming that your
>>> code is
>>> written to target all the systems taken into account by the C
>>> standard, and
>>> if it would invoke nasal demons on any one of them then the compiler
>>> is
>>> allowed to invoke nasal demons on all of them.
>>
>> Sure. However, we think we have to take seriously the fact that a
>> large body of critical code out there is *not* written to target what
>> the C standard is now, and it is very unlikely to be rewritten to do
>> so.
>
> In case you're not aware of it, here's a fairly relevant blog series on
> the topic of undefined behaviour in C:
>
> http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html
> http://blog.llvm.org/2011/05/what-every-c-programmer-should-know_14.html
> http://blog.llvm.org/2011/05/what-every-c-programmer-should-know_21.html

We're aware of those, thanks.

>>
>> At the end of the day, code is not written purely by "thinking about
>> what the C language guarantees", but rather by test-and-debug cycles
>> that test the code against the behaviour of particular C
>> implementations. The ISO C standard is a very loose specification,
>> and we do not have good tools for testing code against all the
>> behaviour it permits,
>
> *cough* -fsanitize=undefined *cough*

That (and other such tools) is surely a *lot* better than what we had
before, no doubt about that. And its developers and those who use it
heavily should be in a good position to comment on our survey
questions, as they are up against the same basic problem, of
reconciling what existing C code actually does vs what compilers
assume about it, to detect errors without too many false positives.
We had quite a few survey responses saying something like "sanitisers
have to allow XYZ, despite the ISO standard, because code really does
it"; in a sense, what we're doing is trying to clearly and precisely
characterise all those cases. If you or others can help with that,
please do!

But such tools are, useful and impressive though they are, aren't
really testing code against all the behaviour ISO permits - as I
understand it, they are essentially checking properties of single
(instrumented) executions, while ISO is a very loose spec, e.g. when
it comes to evaluation order choices and implementation-defined
quantities, permitting many (potentially quite different) executions
for the same source and inputs. Running with -fsanitize=undefined
will detect problems just on the executions that the current compiler
implementation happens to generate. Of course, checking against all
allowed executions of a very loose spec quickly becomes
combinatorially infeasible,

Actually, I would be very, very happy to have an O(2^N) (or worse) algorithm
for checking all allowed executions :wink:

(the problem is actually undecidable; not just "infeasible")

in general, sure :slight_smile: But it quickly becomes infeasible even for
quite small straight-line code.

so this isn't unreasonable, but at lease
we'd like to have that gold standard precisely defined, and to be able
to pseudorandomly check against it.

It sounds like what you are wanting is to basically make a list of undefined
behaviors in the standard and find out which ones, in practice, should be
demoted to unspecified or implementation-defined?

In a sense, yes, though it's more involved than that:
- quite a few of the issues are in areas where the standard is
ambiguous or just doesn't say anything.
- there are many dialects of C (even for a single compiler), e.g. with
and without fno-strict-aliasing, that are semantically quite
different. The standard doesn't attempt to cover those variations.
- in some cases reconciling actual practice and current compiler
optimisation might require new options or annotations, to delimit
places where code can safely do certain things without globally
preventing particular optimisations.

The devil is in the details, and, even after this questionnaire we
still don't have a definitive picture of what clang and gcc do for all
these issues (and there are more...). Hence this thread, to try to
prompt some more serious discussion with the relevant developers...

thanks,
Peter

From: "Sean Silva" <chisophugis@gmail.com>
To: "Peter Sewell" <Peter.Sewell@cl.cam.ac.uk>
Cc: llvmdev@cs.uiuc.edu
Sent: Friday, June 26, 2015 4:53:30 PM
Subject: Re: [LLVMdev] C as used/implemented in practice: analysis of responses

All of these seem to fall into the pattern of "The compiler is
required to do what you expect, as long as it can't prove X about
your program". That is, the only reasonable compilation in the
absence of inferring some extra piece of information about your
program, is the one you expect. For example, the only way to codegen
a comparison between two random pointers has the meaning you expect
(on common computer architectures); but if the compiler can figure
something out that tells it that comparing those two pointers is
undefined by the language standard, then, well, technically it can
do whatever it wants.

Many people interpret this as the compiler being somewhat malevolent,
but there's another interpretation in some cases.

I have not looked in depth at the history in all the undefined
behaviors mentioned in the survey, but some of the undefined
behaviors are there because at some point in time the underlying
system diversity made it difficult or impossible to assign a
meaning. So long as the diversity that led to the desire to leave
something undefined still exists, programs that use those constructs
with certain expectations *will* fail to behave as "expected" on
those targets (on a system where pointers are represented
differently, your program *may* actually format your hard disk if
you do so-and-so!).

To put it another way, what is "expected" is actually dependent on
the C programmer's knowledge of the underlying system (computer
architecture, system architecture, etc.), and there will always be
tension so long as the programmer is not thinking about what the C
language guarantees, but rather (roughly speaking) how *they* would
translate their code to assembly language for the system or systems
that they happen to know they're targeting. An x86 programmer
doesn't expect unaligned loads to invoke nasal demons, but a SPARC
programmer does.

So if you unravel the thread of logic back through the undefined
behaviors made undefined for this reason, many of these cases of
exploiting undefined behavior are really an extension, on the
compiler's part, of the logic "there are some systems for which your
code would invoke nasal demons, so I might as well assume that it
will invoke nasal demons on this system (since the language standard
doesn't say anything about specific systems)". Or to put it another
way, the compiler is effectively assuming that your code is written
to target all the systems taken into account by the C standard, and
if it would invoke nasal demons on any one of them then the compiler
is allowed to invoke nasal demons on all of them.

Honestly, I don't think this argument buys us all that much in the eyes of most programmers. Maybe we're saving them from themselves, maybe not, but while certainly a side effect of exploiting undefined behavior, this is not really why we exploit undefined behavior. We exploit undefined behavior because it helps to optimize real programs and shrink the size of generated code.

Here's a simple example:

int caching_disabled;

static void do_something(struct cache *c) {
  ...

  if (!caching_disabled) {
    ...
    ... c->something ...
    ...
  }

  ...
}

/* This is only called when caching is disabled */
void foo() {
  ...
  do_something(NULL);
  ...
}

a compiler might inline the call to do_something, or specialize it, such that we know that the argument is NULL. But the code in do_something does not check the pointer directly, but instead, checks some other mutable state (that must be related to the pointer for the program to have defined behavior). The code that is inlined into foo(), however, does not need to check '!caching_disabled', but rather because c is unconditionally dereferenced within the block guarded by that check, and because that would necessarily be a dereference of a NULL pointer, and because that is undefined behavior, the compiler can mark that block as unreachable, and can eliminate the check and a potentially-large block of code when inlining.

This kind of application of undefined behavior turns out to be quite helpful in practice. The rules for integer overflow are another great example (loop optimizations don't work nearly as well without them in many cases).

This is obviously sort of a twisted logic, and I think that a lot of
the "malevolence" attributed to compilers is due to this. It
certainly removes many target-dependent checks from the mid-level
optimizer though.

This point is correct and important. Exploiting undefined behavior also simplifies the task of creating target-independent optimizers.

-Hal

From: "Sean Silva" <chisophugis@gmail.com>
To: "Peter Sewell" <Peter.Sewell@cl.cam.ac.uk>
Cc: llvmdev@cs.uiuc.edu
Sent: Friday, June 26, 2015 4:53:30 PM
Subject: Re: [LLVMdev] C as used/implemented in practice: analysis of responses

All of these seem to fall into the pattern of "The compiler is
required to do what you expect, as long as it can't prove X about
your program". That is, the only reasonable compilation in the
absence of inferring some extra piece of information about your
program, is the one you expect. For example, the only way to codegen
a comparison between two random pointers has the meaning you expect
(on common computer architectures); but if the compiler can figure
something out that tells it that comparing those two pointers is
undefined by the language standard, then, well, technically it can
do whatever it wants.

Many people interpret this as the compiler being somewhat malevolent,
but there's another interpretation in some cases.

I have not looked in depth at the history in all the undefined
behaviors mentioned in the survey, but some of the undefined
behaviors are there because at some point in time the underlying
system diversity made it difficult or impossible to assign a
meaning. So long as the diversity that led to the desire to leave
something undefined still exists, programs that use those constructs
with certain expectations *will* fail to behave as "expected" on
those targets (on a system where pointers are represented
differently, your program *may* actually format your hard disk if
you do so-and-so!).

To put it another way, what is "expected" is actually dependent on
the C programmer's knowledge of the underlying system (computer
architecture, system architecture, etc.), and there will always be
tension so long as the programmer is not thinking about what the C
language guarantees, but rather (roughly speaking) how *they* would
translate their code to assembly language for the system or systems
that they happen to know they're targeting. An x86 programmer
doesn't expect unaligned loads to invoke nasal demons, but a SPARC
programmer does.

So if you unravel the thread of logic back through the undefined
behaviors made undefined for this reason, many of these cases of
exploiting undefined behavior are really an extension, on the
compiler's part, of the logic "there are some systems for which your
code would invoke nasal demons, so I might as well assume that it
will invoke nasal demons on this system (since the language standard
doesn't say anything about specific systems)". Or to put it another
way, the compiler is effectively assuming that your code is written
to target all the systems taken into account by the C standard, and
if it would invoke nasal demons on any one of them then the compiler
is allowed to invoke nasal demons on all of them.

Honestly, I don't think this argument buys us all that much in the eyes of most programmers. Maybe we're saving them from themselves, maybe not, but while certainly a side effect of exploiting undefined behavior, this is not really why we exploit undefined behavior. We exploit undefined behavior because it helps to optimize real programs and shrink the size of generated code.

Here's a simple example:

int caching_disabled;

static void do_something(struct cache *c) {
  ...

  if (!caching_disabled) {
    ...
    ... c->something ...
    ...
  }

  ...
}

/* This is only called when caching is disabled */
void foo() {
  ...
  do_something(NULL);
  ...
}

a compiler might inline the call to do_something, or specialize it, such that we know that the argument is NULL. But the code in do_something does not check the pointer directly, but instead, checks some other mutable state (that must be related to the pointer for the program to have defined behavior). The code that is inlined into foo(), however, does not need to check '!caching_disabled', but rather because c is unconditionally dereferenced within the block guarded by that check, and because that would necessarily be a dereference of a NULL pointer, and because that is undefined behavior, the compiler can mark that block as unreachable, and can eliminate the check and a potentially-large block of code when inlining.

This kind of application of undefined behavior turns out to be quite helpful in practice. The rules for integer overflow are another great example (loop optimizations don't work nearly as well without them in many cases).

Yes - but there seems also to be a significant downside of aggressive
exploitation of some of these UBs, at least as reported by OS and
other systems people. The trade-offs between allowing optimisation
and having a language which can be understood (and which supports
common-but-non-ISO idioms) are difficult to manage, but somehow we
(collectively) need to do better than previous decades of C evolution
have.

thanks,
Peter

> From: "Sean Silva" <chisophugis@gmail.com>
> To: "Peter Sewell" <Peter.Sewell@cl.cam.ac.uk>
> Cc: llvmdev@cs.uiuc.edu
> Sent: Friday, June 26, 2015 4:53:30 PM
> Subject: Re: [LLVMdev] C as used/implemented in practice: analysis of
responses
>
>
>
> All of these seem to fall into the pattern of "The compiler is
> required to do what you expect, as long as it can't prove X about
> your program". That is, the only reasonable compilation in the
> absence of inferring some extra piece of information about your
> program, is the one you expect. For example, the only way to codegen
> a comparison between two random pointers has the meaning you expect
> (on common computer architectures); but if the compiler can figure
> something out that tells it that comparing those two pointers is
> undefined by the language standard, then, well, technically it can
> do whatever it wants.
>
>
> Many people interpret this as the compiler being somewhat malevolent,
> but there's another interpretation in some cases.
>
>
>
> I have not looked in depth at the history in all the undefined
> behaviors mentioned in the survey, but some of the undefined
> behaviors are there because at some point in time the underlying
> system diversity made it difficult or impossible to assign a
> meaning. So long as the diversity that led to the desire to leave
> something undefined still exists, programs that use those constructs
> with certain expectations *will* fail to behave as "expected" on
> those targets (on a system where pointers are represented
> differently, your program *may* actually format your hard disk if
> you do so-and-so!).
>
>
> To put it another way, what is "expected" is actually dependent on
> the C programmer's knowledge of the underlying system (computer
> architecture, system architecture, etc.), and there will always be
> tension so long as the programmer is not thinking about what the C
> language guarantees, but rather (roughly speaking) how *they* would
> translate their code to assembly language for the system or systems
> that they happen to know they're targeting. An x86 programmer
> doesn't expect unaligned loads to invoke nasal demons, but a SPARC
> programmer does.
>
>
> So if you unravel the thread of logic back through the undefined
> behaviors made undefined for this reason, many of these cases of
> exploiting undefined behavior are really an extension, on the
> compiler's part, of the logic "there are some systems for which your
> code would invoke nasal demons, so I might as well assume that it
> will invoke nasal demons on this system (since the language standard
> doesn't say anything about specific systems)". Or to put it another
> way, the compiler is effectively assuming that your code is written
> to target all the systems taken into account by the C standard, and
> if it would invoke nasal demons on any one of them then the compiler
> is allowed to invoke nasal demons on all of them.

Honestly, I don't think this argument buys us all that much in the eyes of
most programmers. Maybe we're saving them from themselves, maybe not, but
while certainly a side effect of exploiting undefined behavior, this is not
really why we exploit undefined behavior.

I think you read "all" in some of the places I said "many" or "some". The
example you give below isn't one of the ones I was referring to. Actually,
probably all of the UB that deals with "what happens when you read/write
from <some dubious address>" specifically doesn't fall into the category I
was talking about -- those are true nasal demons and in those situations
the compiler's justification to the programmer is "as far as the language
is concerned, there's really no way to predict what could happen in these
cases, so I have to assume that you don't care about what happens in this
case", which seems pretty reasonable.

We exploit undefined behavior because it helps to optimize real programs
and shrink the size of generated code.

Here's a simple example:

int caching_disabled;

static void do_something(struct cache *c) {
  ...

  if (!caching_disabled) {
    ...
    ... c->something ...
    ...
  }

  ...
}

/* This is only called when caching is disabled */
void foo() {
  ...
  do_something(NULL);
  ...
}

a compiler might inline the call to do_something, or specialize it, such
that we know that the argument is NULL. But the code in do_something does
not check the pointer directly, but instead, checks some other mutable
state (that must be related to the pointer for the program to have defined
behavior). The code that is inlined into foo(), however, does not need to
check '!caching_disabled', but rather because c is unconditionally
dereferenced within the block guarded by that check, and because that would
necessarily be a dereference of a NULL pointer, and because that is
undefined behavior, the compiler can mark that block as unreachable, and
can eliminate the check and a potentially-large block of code when inlining.

This kind of application of undefined behavior turns out to be quite
helpful in practice. The rules for integer overflow are another great
example (loop optimizations don't work nearly as well without them in many
cases).

Integer overflow is one of the ones that IIRC is specifically there for
historical reasons, due to 2's complement not being the unequivocal
standard at the time (that is why overflow UB only applies to signed
numbers). I was actually going to mention how it is a quite happy
coincidence that we got undefined behavior for signed overflow.

-- Sean Silva

>> From: "Sean Silva" <chisophugis@gmail.com>
>> To: "Peter Sewell" <Peter.Sewell@cl.cam.ac.uk>
>> Cc: llvmdev@cs.uiuc.edu
>> Sent: Friday, June 26, 2015 4:53:30 PM
>> Subject: Re: [LLVMdev] C as used/implemented in practice: analysis of
responses
>>
>>
>>
>> All of these seem to fall into the pattern of "The compiler is
>> required to do what you expect, as long as it can't prove X about
>> your program". That is, the only reasonable compilation in the
>> absence of inferring some extra piece of information about your
>> program, is the one you expect. For example, the only way to codegen
>> a comparison between two random pointers has the meaning you expect
>> (on common computer architectures); but if the compiler can figure
>> something out that tells it that comparing those two pointers is
>> undefined by the language standard, then, well, technically it can
>> do whatever it wants.
>>
>>
>> Many people interpret this as the compiler being somewhat malevolent,
>> but there's another interpretation in some cases.
>>
>>
>>
>> I have not looked in depth at the history in all the undefined
>> behaviors mentioned in the survey, but some of the undefined
>> behaviors are there because at some point in time the underlying
>> system diversity made it difficult or impossible to assign a
>> meaning. So long as the diversity that led to the desire to leave
>> something undefined still exists, programs that use those constructs
>> with certain expectations *will* fail to behave as "expected" on
>> those targets (on a system where pointers are represented
>> differently, your program *may* actually format your hard disk if
>> you do so-and-so!).
>>
>>
>> To put it another way, what is "expected" is actually dependent on
>> the C programmer's knowledge of the underlying system (computer
>> architecture, system architecture, etc.), and there will always be
>> tension so long as the programmer is not thinking about what the C
>> language guarantees, but rather (roughly speaking) how *they* would
>> translate their code to assembly language for the system or systems
>> that they happen to know they're targeting. An x86 programmer
>> doesn't expect unaligned loads to invoke nasal demons, but a SPARC
>> programmer does.
>>
>>
>> So if you unravel the thread of logic back through the undefined
>> behaviors made undefined for this reason, many of these cases of
>> exploiting undefined behavior are really an extension, on the
>> compiler's part, of the logic "there are some systems for which your
>> code would invoke nasal demons, so I might as well assume that it
>> will invoke nasal demons on this system (since the language standard
>> doesn't say anything about specific systems)". Or to put it another
>> way, the compiler is effectively assuming that your code is written
>> to target all the systems taken into account by the C standard, and
>> if it would invoke nasal demons on any one of them then the compiler
>> is allowed to invoke nasal demons on all of them.
>
> Honestly, I don't think this argument buys us all that much in the eyes
of most programmers. Maybe we're saving them from themselves, maybe not,
but while certainly a side effect of exploiting undefined behavior, this is
not really why we exploit undefined behavior. We exploit undefined behavior
because it helps to optimize real programs and shrink the size of generated
code.
>
> Here's a simple example:
>
> int caching_disabled;
>
> static void do_something(struct cache *c) {
> ...
>
> if (!caching_disabled) {
> ...
> ... c->something ...
> ...
> }
>
> ...
> }
>
> /* This is only called when caching is disabled */
> void foo() {
> ...
> do_something(NULL);
> ...
> }
>
> a compiler might inline the call to do_something, or specialize it, such
that we know that the argument is NULL. But the code in do_something does
not check the pointer directly, but instead, checks some other mutable
state (that must be related to the pointer for the program to have defined
behavior). The code that is inlined into foo(), however, does not need to
check '!caching_disabled', but rather because c is unconditionally
dereferenced within the block guarded by that check, and because that would
necessarily be a dereference of a NULL pointer, and because that is
undefined behavior, the compiler can mark that block as unreachable, and
can eliminate the check and a potentially-large block of code when inlining.
>
> This kind of application of undefined behavior turns out to be quite
helpful in practice. The rules for integer overflow are another great
example (loop optimizations don't work nearly as well without them in many
cases).

Yes - but there seems also to be a significant downside of aggressive
exploitation of some of these UBs, at least as reported by OS and
other systems people. The trade-offs between allowing optimisation
and having a language which can be understood (and which supports
common-but-non-ISO idioms) are difficult to manage, but somehow we
(collectively) need to do better than previous decades of C evolution
have.

In the particular example Hal gives it seems like most users would probably
prefer the compiler require them to add an explicit assertion of c's
validity in the block dominated by !caching_disabled.

Unfortunately in other cases it is very hard to communicate what the user
should assert/why they should assert it, as Chris talks about in his blog
posts. So it realistically becomes sort of black and white -- either don't
optimize based on UB or do. For what is probably just social reasons, the
desire to optimize wins out; from an economic standpoint (e.g. power saved)
it overall may be the right choice (I haven't run any ballpark figures
though and don't claim this to be true).

Ideally together with the compiler there would be a static analyzer with
the invariant that it finds all situations where the compiler, while
compiling, optimizes based on UB. This static analyzer would report in an
intelligible way on all such situations. Unfortunately this is a really
hard problem.

-- Sean Silva

Unfortunately in other cases it is very hard to communicate what the user
should assert/why they should assert it, as Chris talks about in his blog
posts. So it realistically becomes sort of black and white -- either don't
optimize based on UB or do. For what is probably just social reasons, the
desire to optimize wins out; from an economic standpoint (e.g. power saved)
it overall may be the right choice (I haven't run any ballpark figures
though and don't claim this to be true).

This is *so* true. There's a natural progression of programmers as
they age. Initially, people are adverse of side effects and they hate
"misbehaviours" from their compiler. As time passes and their
experiences grow, they start to like some of the side effects, and as
maturity reaches them, they are already *relying* on them. C/C++
undefined behaviour and Perl's utter disregard for clarity are some of
the examples.

Chandler said something at the last US LLVM meeting that stuck with
me: "you guys expect hardware to behave in ways that hardware can't".
Undefined behaviour and implementation defined features in the C/C++
standards is what it is, on purpose. If it wasn't for that, C/C++
couldn't perform well on most hardware architectures of today.
Programmers *must* learn not to rely on their particular desires or
compilers, to understand the language for what it is, and to exploit
its perks while still being platform independent. It is possible, but
*very* hard.

Ideally together with the compiler there would be a static analyzer with the
invariant that it finds all situations where the compiler, while compiling,
optimizes based on UB. This static analyzer would report in an intelligible
way on all such situations. Unfortunately this is a really hard problem.

And that's why you have static analyser tools! Lints, checks,
sanitizers, warnings and error messages are all there to make you into
a better programmer, so you can learn about the language, and how to
use your compiler.

Ultimately, compilers are tools. The sharper they get, the more
carefully you need to handle it. They also have a safety trigger: it's
called -O0.

cheers,
--renato

Unfortunately in other cases it is very hard to communicate what the user
should assert/why they should assert it, as Chris talks about in his blog
posts. So it realistically becomes sort of black and white -- either don't
optimize based on UB or do. For what is probably just social reasons, the
desire to optimize wins out; from an economic standpoint (e.g. power saved)
it overall may be the right choice (I haven't run any ballpark figures
though and don't claim this to be true).

This is *so* true. There's a natural progression of programmers as
they age. Initially, people are adverse of side effects and they hate
"misbehaviours" from their compiler. As time passes and their
experiences grow, they start to like some of the side effects, and as
maturity reaches them, they are already *relying* on them. C/C++
undefined behaviour and Perl's utter disregard for clarity are some of
the examples.

Chandler said something at the last US LLVM meeting that stuck with
me: "you guys expect hardware to behave in ways that hardware can't".
Undefined behaviour and implementation defined features in the C/C++
standards is what it is, on purpose. If it wasn't for that, C/C++
couldn't perform well on most hardware architectures of today.

I fear that this:

Programmers *must* learn not to rely on their particular desires or
compilers, to understand the language for what it is, and to exploit
its perks while still being platform independent. It is possible, but
*very* hard.

while attractive from the compiler-writer point of view, is just not
realistic, given the enormous body of C code out there which does
depend on some particular properties which are not guaranteed by the
ISO standard.
That code is not necessarily all gospel, of course, far from it - but
its existence does have to be taken seriously.

Peter

while attractive from the compiler-writer point of view, is just not
realistic, given the enormous body of C code out there which does
depend on some particular properties which are not guaranteed by the
ISO standard.

There is also an enormous body of code that is just wrong. Do we have
to worry about getting that right, too? Trying to "understand" the
authors' intentions and do that instead of what they asked?

Where do we draw the line? What do we consider "a reasonable
deviation" from just "plain wrong"?

There is a large portion of non-standard documented behaviours in all
compilers, and GCC and Clang are particularly important here. Most
builtin functions, attributes, and extensions are supported by both
compilers in a similar way, and people can somewhat rely on it. But
the only true reliable sources are the standards.

However, the very definition of undefined behaviour is "here be
dragons", and that's something that was purposely done to aid
compilers at optimising code. You may try to unite the open source
compilers in many ways (as I tried last year), but trying to regulate
undefined behaviour is not one of them.

That code is not necessarily all gospel, of course, far from it - but
its existence does have to be taken seriously.

And we do! Though, in a completely different direction than you would expect. :slight_smile:

You advocate for better consistent support, which is ok and I, for
one, have gone down that path multiple times. But in this specific
case, the way we take it seriously is by warning the users of the
potential peril AND abuse of it for performance reasons. This is a
sweet spot because novice users will learn the language and advanced
users will like the performance.

cheers,
--renato

Sounds like endless compiler writers vs. maintainers dispute.

-Y

That is the reason compilers exploit undefined behaviour even when they are
generating code for a vanilla architecture with a flat address space, yes.
However, I will suggest:

1. The performance gain from this on real programs is small. I will suggest
that the total performance gain from optimisations that rely on exploiting
undefined behaviour - let's call them monkey's paw optimisations for short
- is practically never more than a few percent, and often less than one
percent.

2. For most programs, having the program work is worth far more than having
it run a few percent faster.

3. If you look at the survey answers, it's clear that most real programs,
whether deliberately or accidentally, invoke undefined behaviour at some
point. You could say this is the programmer's fault, but in practice,
miscompiling a program typically punishes people other than the programmer
who wrote the code in question (and who may at this stage be long gone).
Furthermore, so little behaviour is actually defined by the letter of the C
and C++ standards, that the probability of even a team of highly skilled
and conscientious programmers writing a million line program without ever
invoking undefined behaviour is for all practical purposes zero.

I am frankly of the opinion that monkey's paw optimisations cause so much
trouble and are so difficult (for all practical purposes impossible) to
avoid tripping over, that it would be better to remove them entirely, but
given that compiler maintainers are clearly unwilling to do that, I propose
the following compromise:

Group all monkey's paw optimisations together, and enable them only if an
extra compiler flag is supplied. Or failing that, at least have a compiler
flag that will disable all of them (while leaving all the safe
optimisations enabled).

while attractive from the compiler-writer point of view, is just not
realistic, given the enormous body of C code out there which does
depend on some particular properties which are not guaranteed by the
ISO standard.

There is also an enormous body of code that is just wrong. Do we have
to worry about getting that right, too? Trying to "understand" the
authors' intentions and do that instead of what they asked?

Where do we draw the line? What do we consider "a reasonable
deviation" from just "plain wrong"?

It varies from case to case, and one has to be pragmatic. But from
what we see, in our survey results and in Table 1 of
http://www.cl.cam.ac.uk/~dc552/papers/asplos15-memory-safe-c.pdf,
there are a number of non-ISO idioms that really are used pervasively
and for good reasons in systems code.

- some are actually supported by mainstream compilers but not
documented as such, e.g. where the ISO standard forbade things for
now-obsolute h/w reasons. For those, we can identify a
stronger-than-ISO mainstream semantics. For example, our Q12, making
a null pointer by casting from an expression that isn't a constant but
that evaluates to 0, might be in this category.

- some are used more rarely but in important use-cases, and we could
have options to turn off the relevant optimisations, or perhaps
additional annotations in the source types, that guarantee they work.
For example, our Q3 "Can one use pointer arithmetic between separately
allocated C objects" may be like this.

- for some, OS developers are already routinely turning off
optimisations for the sake of more predictable semantics, e.g. with
fno-strict-aliasing.

- for a few (e.g. our Q1 and Q2, and maybe also Q9 and Q10), there are
real conflicts, and it's not clear how to reconcile the compiler and
systems-programmer views; there we're trying to understand what's
possible. That might involve restricting some optimisations (and one
should try to understand the cost thereof), or additional options, or
documenting what compilers already do more clearly.

There is a large portion of non-standard documented behaviours in all
compilers, and GCC and Clang are particularly important here. Most
builtin functions, attributes, and extensions are supported by both
compilers in a similar way, and people can somewhat rely on it.

But
the only true reliable sources are the standards.

Sadly the ISO standards are neither completely unambiguous nor a good
guide to what can be or is assumed about implementations. (I say
this having contributed to the C/C++11 standards.)

However, the very definition of undefined behaviour is "here be
dragons", and that's something that was purposely done to aid
compilers at optimising code. You may try to unite the open source
compilers in many ways (as I tried last year), but trying to regulate
undefined behaviour is not one of them.

That code is not necessarily all gospel, of course, far from it - but
its existence does have to be taken seriously.

And we do! Though, in a completely different direction than you would expect. :slight_smile:

You advocate for better consistent support, which is ok and I, for
one, have gone down that path multiple times. But in this specific
case, the way we take it seriously is by warning the users of the
potential peril AND abuse of it for performance reasons. This is a
sweet spot because novice users will learn the language and advanced
users will like the performance.

What we see in discussions with at least some communities of advanced
users is not completely consistent with that, I'm afraid...

thanks,
Peter

So, are you suggesting we get rid of all undefined AND implementation
defined behaviour from compilers?

That means getting:
* all compiler people to agree on a specific interpretation, then
* all hardware people to re-implement their hardware, re-ship their products

Unless there is a flag, say, -std=c11, which makes the compiler follow
the standard?

If not all, how pragmatic is good pragmatic? How much of it should we
do? How are we going to get all compiler folks from all fields to
agree on what's acceptable and what's not?

Funny enough, there is a place where that happens already, the C/C++
standard committee. And what's left of undefined / implementation
defined behaviour is what they don't agree on.

I can't see how this could be different...

cheers,
--renato

Not at all. As you say, that would require all compiler implementers to
agree, and what little behaviour is defined in the standards is presumably
already what all compiler implementers can agree on.

I'm proposing that LLVM unilaterally replace most undefined behaviour with
implementation-defined behaviour.

Note that this would give it a substantial competitive advantage over GCC:
a lot of people care far more about reliability than about tiny increments
of performance.

That's precisely the problem. Which behaviour?

Let's have an example:

struct Foo {
  long a[95];
  char b[4];
  double c[2];
};

void fuzz(Foo &F) {
  for (int i=0; i<100; i++)
    F.a[i] = 123;
}

There are many ways I can do this "right":

1. Only go up to 95, since you're using an integer to set the value.
2. Go up to 96, since char is an integer type.
2. Go all the way to 100, but casting "123" to double from 97 onwards, in pairs
3. Go all the way to 100, and set integer 123 bitwise (for whatever fp
representation that is) from 97
4. Do any of above, and emit a warning
5. Bail on error

Compilers prefer not to bail on error, since the standard permits it.
A warning would be a good thing, though.

Now, since it's a warning, I *have* to output something. What? Even
considering one compiler, you'll have to convince *most* <compilerX>
engineers to agree on something, and that's not trivial.

Moreover, this loop is very easy to vectorise, and that would give me
4x speed improvements for 4-way vectorization. That's too much for
compilers to pass.

If I create a vectorised loop that goes all the way to 92, I'll have
to create a tail loop. If I don't want to create a tail loop, I have
to override 'b' (and probably 'c') on a vector write. If I implement
the variations where I can do that, the vectoriser will be very happy.
People generally like when the vectoriser is happy.

Now, you have a "safe mode" where these things don't happen. Let's say
you and me agree that it should only go to 95, since this is "probably
what the user wants". But some programmers *use* that as a feature,
and the standard allow it, so we *have* to implement it *both*.

Best case scenario, you have now implemented two completely different
behaviours for every undefined behaviour in each standard. Worse
still, you have divided the programmers in two classes: those that
play it safe, and those that don't, essentially creating two different
programming languages. Code that compiles and work with
compilerA+safe_mode will not necessarily compile/work with
compilerB+safe_mode or compilerA+full_mode either.

C and C++ are already complicated enough, with so many standard levels
to implement (C90, C99, C11, C++03, C++11, C++14, etc) that
duplicating each and everyone of them, *per compiler*, is not
something you want to do.

That will, ultimately, move compilers away from each other, which is
not what most users really want.

cheers,
--renato