"Alf P. Steinbach" <
alf.p.stein...@gmail.com> writes:
> On 20-Jun-17 4:28 PM, Tim Rentsch wrote:
>
>>
leigh.v....@googlemail.com writes:
>>
>>> One shouldn't rely on compiler optimizations to ensure code
>>> correctness. Rather than using recursion write an explicit loop if
>>> there is likelihood of lots of iterations.
>>
>> [..and also..]
>>
>>> One should not write code in a way where correctness isn't
>>> guaranteed. Testing and peer review does not guarantee code
>>> correctness.
>>
>> Point one: the concern here is not one of correctness but
>> one of resource requirements.
>
> When you exceed available machine stack space you get UB.
This is a common misconception, but actually it isn't right. The
abstract machine has no notion of stack, and no notion of running
out of stack space. The semantics of function calls is defined,
without regard to how much "stack space" is available, and there
is no excepting statement to make one undefined (assuming of
course the type of the call matches the type of the definition,
which is not at issue here).
In practical terms, the consequences of running out of stack
space may be just as severe as undefined behavior. But the
distinction is still important, because the issue is not a
language question but rather is extra-linguistic, ie, it depends
on things outside the language definition, and indeed on things
outside the implementation.
If you want to check this out, look at section 1 paragraph 2 in
the C standard (ie, the one that starts "This International
Standard does not specify ..."). AFAICT there is no similar set
of provisions in the C++ standard, but the C++ standard
incorporates the C standard as a normative reference, which I
believe is meant to make the same assertions for C++ as are
made for C in section 1 paragraph 2.
> Correctness is therefore not a separate issue from resource
> requirements.
>
> It's not separate even with a single compiler or small set of
> compilers, for a new version or different options, or really
> anything (since there is no guarantee), can foil the crucial
> optimization of tail recursion.
Maybe you mean something different by "correctness" than I do.
What I mean is that the semantics of the language gives the
program a meaning that agrees with its desired effect. With this
meaning of correctness, the issues here are not about correctness
but about other factors.
Now I grant you, those other concerns are important, and can
matter just as much as having a wrong program or one that has
undefined behavior. I don't mean to dismiss them, just be clear
about where the issues lie. (And we will take up shortly how
they may be addressed.)
>> Point two: whether one should rely on "compiler optimizations"
>> to achieve a desired effect depends on the particular case
>> involved. It is just as wrong to never rely on what a compiler
>> will do as it is to always rely on what a compiler will do.
>
> This seems reasonable.
>
>
>> Point three: in most environments it is easy to perform a static
>> automated check to verify that the code produced will behave as
>> expected for functions like the tail-recursive example shown
>> upthread. (And more complicated examples aren't that much
>> harder.) This check provides the needed guarantee, without the
>> need of any test cases or peer review.
>
> How much work is it to implement such a check?
>
> It would be nice if you could provide one, to put this argument on
> a more solid foundation.
Let me outline a way for the simple case (but probably also the
most common) of self-call. Afterwards there may be some comments
about more elaborate cases.
1. When compiling, have the compiler leave generated
assembly around for the checking step.
2. Scan the generated assembly, doing this:
2a. When a function starts, remember its name
2b. When a function ends, remember it is over
2c. When a 'call' instruction is encounted, if
the call is to the same function as the
last one started, the call is a self-call,
and should be flagged
2d. At the end, if no self-calls have been
flagged, there are no "bad" functions.
I expect you can write one of these yourself, without too much
difficulty. I wrote one in awk that's about 50 lines. (I make
no apologies for my awk code, but I must confess it isn't always
the code I'm most proud of. :) You're a code hound - how about
you try coding that up (in C++ if that can be done easily) and
see how it goes?
There are several limitations that are worth mentioning (listed
roughly in order of increasing difficulty).
1. Some functions, eg quicksort, are meant to be truly recursive
(ie, have self-calls). If we want to allow that there needs to
be a way to identify those functions so they won't be flagged.
2. Scanning the assembly is target specific. If there are
multiple target platforms then there needs to be a scanning
program for each target (or perhaps a single program that
understands many targets).
3. If we want to identify more general patterns, eg foo() calls
bas() and bas() calls foo(), the analysis is more complicated.
Code to flag the more general cases certainly isn't trivial,
but I think it isn't too hard either (similar to call graph
analysis).
4. The scheme outlined looks at one TU at a time, but not
inter-TU mutual recursion. If it's important to identify
"bad" inter-TU mutual recursion, then the analysis needs
to be done on all the generated assemblies at once (or maybe
produce some sort of summary, which can then be combined at
the end for the inter-TU stuff).
5. Indirect function calls (eg, through pointer-to-function
types) don't get flagged (ie, assuming there isn't some sort of
flow analysis that figures out which actual function is called).
This could matter for C++, if we want to identify mutually
recursive virtual functions. Actually I think there is something
useful that can be done here, but for simplicity let's assume
this case is out of bounds (so no large recursion depths for
mutually recursive virtual functions).
It takes a lot to do all of these, but there's no reason we have
to do that. Taking just (1) and (2), that's probably another 10
lines plus maybe 15 lines per target. I should probably go off
and do an implementation for (3) rather than speculate about how
much code it would be. But even without (3) we have a very
useful tool, because direct self-call is what will come up most
often, especially as people start exploring functional/recursive
alternatives.