13
$\begingroup$

In C, most binary operators do not specify which operand will be evaluated first:

int x(void) {
    putchar('x');
    return 10;
}
int y(void) {
    putchar('y');
    return 10;
}

// ...

x()+y(); // Unspecified behavior as this may print xy or yx

Many other languages make guarantees that the operands will be evaluated left to right, so the equivalent code in Java is guaranteed to print: xy

Leaving things like these unspecified in C is often done for optimization reasons, so are there any optimizations possible for the compiler to perform when sequence points are not defined between operators such as + = and similar?

$\endgroup$
7
  • 4
    $\begingroup$ It's worth noting that, as a consequence of not defining the order for two operands and one operator, an expression like a() + b() + c() + d() + ... + z() could evaluate the operands in any permutation. So they could be reordered to optimise cache use if e.g. a() and z() both wanted to read from the same area of memory. Another possibility is a() + b() + a() where duplicating the result of a() on the stack might be faster than storing it to a temporary variable and duplicating it later. $\endgroup$ Commented Feb 1 at 0:46
  • 8
    $\begingroup$ Also it is important to remember that often C leaves stuff underspecified not primarily to enable optimizations, but rather to weaken the standard enough to ensure that many existing implementations of C can say that they conform! Many incompatible implementations available before the standard is written is an unfortunate consequence of early adoption. $\endgroup$ Commented Feb 1 at 1:23
  • 2
    $\begingroup$ Another unfortunate consequence is that the gaps and ambiguities introduced into language standards often end up exploited in new ways by compiler writers for "optimisation". This is without regard to prior de-facto behaviour of incumbent compilers, the behaviour of which the gaps were formed to accomodate. And it is without regard to whether the language actually remains tractable by the programmer under the new interpretation of the language by that compiler-maker (i.e. it is without regard to one of the main concerns of a language designer - usability). (2/2) $\endgroup$ Commented Feb 1 at 12:12
  • 2
    $\begingroup$ x*y+f() might be better in machine code as f()+x*y, as x*y requires a temporary, which if done before f() will be live across the call. On the other hand if x and y are not themselves used after this line, then the x*y+f() would release them both, while requiring a temporary to be live across the call, whereas f()+x*y would require no temporary but x and y both live across the call... $\endgroup$ Commented Feb 1 at 19:17
  • 2
    $\begingroup$ If we're being pedantic, under C semantics, the executions of x() and y() in your example are indeterminately sequenced: it's either all of x() then all of y(), or vice versa. Having them be unsequenced would be much more aggressive: the compiler would be allowed to interleave their executions, or execute them in parallel in separate threads, etc. The programmer would have to ensure that this didn't cause any data races, or else the behavior would be undefined. Obviously this would allow for many more optimizations, but also make it very hard to write correct code. $\endgroup$ Commented Feb 3 at 16:09

2 Answers 2

15
$\begingroup$

Register allocation

This may appear to be a weird answer, but consider these other questions: How many registers are necessary to arrange and call these functions? How many registers to keep alive or spill to memory, to care for intermediate results? What registers are clobbered in general or in this particular case? How much do these questions change on other machines?

These questions need not be answered in general, because C was created to abstract away these machine details and ABI conventions. Yet, C is about generating assembly and binary objects, where these questions are paramount.

Having all this unspecified allows for all sorts of micro optimizations at register allocation level.

And, I suspect, also allows some other tricks at source level, because many compound expressions can be freely (and fully) reordered then is possible, in principle, to examining all combinatorial arrangements of unsequenced subexpressions, to find some combination that uses less or spills less registers in the alternatives.

Divergent incompatible historical implementations

I would also echo the excellent and eloquent points of Eric Lippert and Steve on the comments of the question (and encourage them to elaborate in answers), to point out that the history of C plays a very salient role here.

These details are underspecified on the standard of the language, not to enable unspecified future optimizations, but to accommodate various divergent incompatible historical behaviours in various compilers. Where in one vendor, compiler, or particular machine, one particular optimization is culturally acceptable, desirable and even necessary, in another it was not.

But by the time of language standardization, these incompatibilities were not or could not be worked on, so most of these historical incompatibilities optimizations were left intentionally unspecified.

$\endgroup$
4
$\begingroup$

In many cases, an assignment expression which contains any function calls will need to save one or more temporary values around all but one of the function calls. Putting a function call before any other part of the expression evaluation will avoid the need to store any values related to the evaluation across the call.

For example, given extern int *a,*b,*f(void);, all of the assignments *a=*f()+*b;, *a=*b+f();, and *f() = *a+*b; would be processed most efficiently by performing the function call before the loads of a and b, despite the fact that the function calls appear at different places within the expression.

Performing function calls early is often not considered an "optimization" as such, but may be performed as part of basic code generation if a tree-based expression handler distinguishes between "simple" and "complicated" operands, and processes complicated operands before simple ones.

$\endgroup$
1
  • 1
    $\begingroup$ @kaya3: Is it clearer now? $\endgroup$ Commented Feb 10 at 20:39

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.