T
Tim Rentsch
[snip]Apparently several people are of the opinion that having language
semantics be more deterministic is better than being not as
deterministic, because... well, just because. To that I say,
just 'tisn't so.
Even if a clever optimizing compiler could recover (relative to C as
it is now) all the possible parallelism of a C-like language with a
defined left-to-right order of evaluation (and it can't, but never
mind that now), it still isn't automatically better to impose a
left-to-right evaluation order, or any fixed evaluation order. In
fact, doing that to C expressions would make C a worse language,
not a better one. Here's why.
If I see a piece of C code like, for example,
then I don't have to look at the definitions for g() and h() to know
they don't interact (by which I mean, interact in any nontrivial
way). The reason? If they did interact, the code would have been
written differently, to force a particular order of evaluation.
Of course, technically I don't know that g() and h() don't interact;
I know only that the person who wrote the code didn't think it was
important to force a particular order of evaluation. But knowing
the intent (or in this case, the lack of intent) of the code's
author is just as valuable here, or perhaps more valuable. I can
always go look at the definitions for g() and h() to see if they
interact, but I can't go back and look at what the code's author
was thinking.
Now consider the case where the language specifies a left-to-right
evaluation order. Let's look again at the example line. Now I have
to wonder if g() and h() interact; to find out I have to go read
their definitions. If they don't interact, I can breathe a big sigh
of relief and go back to reading the function where they were
called. But suppose they do interact; in that case I have to
wonder if the interaction was known and deliberate, or whether it
might have been unintentional. Short of going back and asking the
original author, there really isn't any way of knowing. Discovering
what the program does and what the program was intended to do has
become a lot more work.
Conversely, if I discover that g() and h() interact in C as it
is now, it's simply an error. The original programmer either
forgot something, or misunderstood something, or was confused
about something, or whatever; which ever of these is these is
the case, the program is simply in error, and I don't have to
wonder whether the interaction was intended or not -- it wasn't.[1]
Requiring a left-to-right evaluation order has made the job of code
reading harder rather than easier. And it encourages, even if only
indirectly, the writing of non-obvious code that depends on that
evaluation order. Both of those trends are trending in the wrong
direction.
[1] Of course, it's possible to construct situations where g() and
h() interact in a non-trivial way, yet the overall program behavior
is correct no matter which order is chosen. But the vast majority
of cases are not this way; moreover, any programmer who writes such
code without putting in a comment on that aspect deserves no better
treatment than a programmer who made an outright error, or at the
very least should be prepared for some sharp criticism during code
review.
It's an interesting argument, but in my incredibly reliable
opinion, not a sound argument, either from a computer science
perspective, or from a software engineering perspective.
The notion that code such as
a = f( g(x), h(y) );
implies that the code reader has a right to expect that g and h
do not interact is in the nature of a pious expectation. There
is no contract here, no certification by the writer of the code,
no enforcement mechanism, and, for that matter, it is possible
that the writer of the code cannot know whether there is any
interaction.
One can argue just as well that unspecified order of evaluation
makes code reading harder rather than easier; when the order is
undefined the number of possible interpretations increases
substantially. In C it is suprisingly easy to write code that
invokes undefined behaviour, easy in quite unexpected and subtle
ways. Quite often the source of the undefined behaviour is
rooted in the looseness of ordering. The upshot is that
undefined order of evaluation is a source of coding error and a
source of increased code reading work.
There is one other way in which undefined order of evaluation
makes code reading harder; it is harder to walk through code and
"play computer" because the number of paths through the code
explodes.
There is an old maxim, KISS, that applies here. Undefined order
of evaluation violates that maxim. It takes extra expertise to
write and read code that is expected to work in an environment
where undefined order or evaluation is the norm. That expertise
may be desirable in special situations but that is no reason for
it to be a universal requirement.
There is a more general software engineering issue here. In
practice there are three main routes to creating robust software
- coding practice, testing, and use of trusted components. Good
coding practice is important, and desk checking, aka, code review
is valuable. The simple truth, though, is that it doesn't catch
everything.
Another important tool for creating robust software is testing,
both unit testing and regression testing. Now comes the
inconvenient truth; testing is not proof against order of
evaluation effects. Code may work perfectly when compiled with
one (conforming) compiler or group of compilers and yet fail when
compiled with others. Moreover it may pass all tests today and
yet fail tomorrow because of a change when your compiler(s) were
upgraded. In short, undefined order of evaluation degrades the
effectiveness of testing.
And, sadly, it also degrades the trustworthiness of trusted
components - in the nature of things, if code practice is
compromised and testing is compromised, then trustworthiness is
compromised.
In short, undefined order of evaluation in C is undesirable and
is not really necessary for compiling efficient code. However C
is what it is and is not likely to change, so the issue is
academic. It and the associated little bits of klutziness are
just something C programmers have to live with.
First let me try to summarize your comments.
1. My argument is flawed (or unsound) as regards f(g(x),h(y)):
a. no guarantee that g(x) and h(y) do not in fact interact.
b. the reader has no right to expect that they don't interact.
c. no guarantee that the author intended that they don't interact.
d. no guarantee that the author thought about interaction at all.
2. My argument is flawed as regards ease of reading:
a. Unspecified OoE => number of possible interpretations goes up.
b. Unspecified OoE often is the cause of undefined behavior.
c. Unspecified OoE make it harder to play computer.
3. Unspecified OoE isn't as simple as settling on one particular
OoE. (The "KISS" principle.)
4. Three main techniques for creating good software: coding
practice, testing, "trusted components" (which I took to
mean modularity). Unspecified OoE degrades, or is not
effectively dealt with, all three.
5. (Because of all the above) Unspecified OoE is undesirable.
6. Unspecified OoE is unnecessary for compiling efficient code.
7. All the above is (most probably) moot since in all likelihood
C won't change on this aspect.
Responding to the above, in order (or mostly in order) --
Comments 1a and 1b mischaracterize my position; I trust reading
my earlier posting makes that clear. Taken as stand-alone
statements (that is, including the "no" on each lettered line,
and not including the prefacing clause "My argument ..."),
statements 1a and 1b are true.
Comments 1c and 1d may reflect what I literally said (and if so
I'm sorry for the confusion), but they don't reflect the position
I was trying to convey. Of course we have no guarantees what the
original author intended or thought about. What we do know is an
either/or: either the author expects that g(x) and h(y) should
not interact, or the author is confused about how C works.
Similarly, as to actual program behavior, again an either/or:
either g(x) and h(y) don't interact (ie, in an interfering way),
or the program is wrong.
By analogy, it's very much like seeing an expression 'a' that
does array indexing. We don't know that the variable 'i' is
in range for the array 'a' that it's indexing; but either it
is in range, or the program is wrong. Similarly, we don't know
that the author expected 'i' to be in range for 'a'; but we
do know that he should have expected it to be within range,
or he is confused about how C works.
For 2a, the statement presupposes that it makes sense to consider
interpretations with non-trivial interaction as being meaningful.
With unspecified order of evaluation, there still is only one
interpretation, and that's the same interpretation as if we do
evaluation left-to-right (or right-to-left for that matter). Of
course, there is in addition the possibility that subexpressions
interact, but what that means is that the program is wrong.
Again this is similar to 'a' -- we do need to check that 'i'
will be within range, but we don't need to think about what
happens if it isn't, because at that point we already know the
program is just wrong.
For 2b -- yes, undefined OoE can be a source of undefined
behavior. It's hard to consider this a serious objection,
because it's so easy to guard against it, by using assignment
statements. I expect most developers use a compiler switch that
warns them when a variable might be used uninitialized, and when
they get these warnings (if they are sensible) change the code so
that the warnings don't happen; the "when it doubt, spell it
out" principle. It's easy to apply this principle to potential
problems with order of evaluation.
For 2c -- it's hard to know what to say about this, because the
argument seems so silly. Yes, at some purely local level, it's
easier to simulate a deterministic mechanism than a different
mechanism that imposes constraints on the program (and fails if
those constraints are not met). But doing this doesn't really
get us anything, except more effort spent simulating. By
contast, with order of evaluation being unspecified, now when we
get to an expression we can check to see if the results depend on
the order of evaluation, and if they do, we can stop simulating
altogether. Making the simulation effort easier on a local level
hasn't made the overall task any easier; in fact usually it will
only make the overall task take longer.
For 3... First, for the sake of discussion I'll agree to
stipulate that the KISS principle should be observed in this
instance. But now comes the important second question -- what is
it that's important to keep simple? Allowing the rule used in
the language definition to be simple results in programs being
more complicated, because expressions may legally depend on
evaluation order; conversely, having a rule that is more
formally difficult used in the language definition results in
programs being simpler, because in legal programs expressions
aren't allowed to depend on evaluation order. Simplifying the
space of legal programs is the more desirable metric here. It's
true that by doing that we have (at least to some degree) raised
the difficulty of /writing/ legal programs; but the programs
we've excluded are programs that are harder to understand than
the ones still allowed, and that tradeoff is the important one to
make in this circumstance. "Easy writing makes hard reading, and
vice versa" -- correct for programming as well as for regular
prose.
For 4 -- this one surprised me, because all of these can
be brought to bear (and relatively easily) on any potential
problems having to do with evaluation order.
For coding practices: first, on the process side, there is
visual inspection (which I think you noted at some point); and
second, during development, there are coding habits that can be
used very effectively (such as: put state-changing operators
only at the top level, don't access global variables in
expressions that also call functions, or allow function calls
along at most one branch of a multi-path operator (ie, like +,
but not like &&), among others).
For testing: automated inspection (a code analysis tool to
detect potential order dependencies -- more on this below). (Yes
I know some people wouldn't call this testing; it depends on
whether it's thought of as part of the development process or as
part of the QA process, but whatever.) Another tool is a C-to-C
translator that imposes an evaluation order on all expressions --
could produce a L-to-R version, an R-to-L version, a version that
chooses randomly at each expression between one or the other, and
compare each of these against the unaltered program during
regression testing.
For modularity: for writing new code -- any function that
modifies global state can be made a 'void' function. For using
existing code -- make lists of functions in each module that (a)
modify global state, or (b) depend on global state for their
output; these lists can be consulted when reading client code,
to easily detect potential problems. Both of these techniques
will strengthen the quality of the modules provided, and not just
with respect to evaluation order.
For 5 -- I think I've made it clear above that (IMO, anyway) the
benefits for unspecified OoE were underestimated, and the costs
for preventing the problems are really not that high. But
there's another key factor, and that is the cost of writing
programs under a required (particular) OoE, and that cost is very
high. In general it's a bad idea to write code that depends on
order of evaluation, even if the order is well-defined. Also,
requiring a particular order of evaluation would distort how
expressions are written in certain cases; consider cases such
as (these examples are written using function calls, but other
state-changing operators such as ++ could also be substituted):
f() - g() OR -g() + f()
f() < g() OR g() > f()
f()[g()] OR g()[f()] (one function returns an int,
the other a pointer).
You know (meant in the generic sense of "you") that cases like
these will start to crop up the moment that there's a difference
between the forms in each pair; some developers won't be able to
resist the lure of writing "clever" code that gives such succinct
expression. Requiring a particular order of evaluation yields a
cost both in code comprehension and code modification; it would
be (please excuse me for choosing such an emphatic next word)
foolish to discount these costs or pretend they don't exist.
For 6 -- a statement along the lines of "we can still compile
efficient code" is interesting, because actually it undercuts the
argument. If it's true that clever optimizers can discover which
cases can have their orders of evaluation changed and which ones
cannot, then it's also possible to write a tool to detect where
unspecified order of evaluation might be a problem; it's the
same question! Conversely, if we can't discover such cases
automatically, then having a particular order of evaluation be
required means compilers won't be able to generate code that's as
efficient as having order of evaluation be unspecified. Either
way, it weakens the case for defining evaluation order.
For 7 -- Certainly I agree that C is unlikely to change on this
point. However, I think there being existing code doesn't change
this very much, because no working program has its meaning
changed if evaluation order were fixed. Rather, the resistance
would come because (a) the argument would be made (whether
rightly or wrongly) that efficiency would suffer, but also (b)
the resulting language would encourage worse programming,
resulting in worse programs. This problem compounds with time;
even if a program with order-of-evaluation dependency can be
understood when it's first written, as maintenance occurs it's
only a matter of time before how it works isn't understood by
anyone.
I'd like to ask people who favor defining order of evaluation
to consider the commentary above, and then would ask: Does
this affect your position? (If so of course I'm happy to hear
it but...) if not then why not?