Guys,
I'm interested in how the compiler implements function calls, can anyone
correct my understanding/point me towards some good articles.
First of all, it depends on the compiler. The FAQ for comp.compilers
is probably a good place to start for information on how such things
are done in general.
What you can count on, as guaranteed by the C++ language:
When you call a function, the implementation arranges for the arguments
(if anything) to be made available to the code in the function.
(In the case of a compiler (as opposed to an interpreter, say) it does
this by generating code (at compile time) to do this (at run time).)
For reference arguments, what's made available to the function is
the information needed to find the object passed by reference; for
non-reference arguments, what's made available is a copy of the object.
Inside the function, local (automatic) variables are created in some
area referenced by some sort of invocation record. These invocation
records act stack-like (and are therefore implemented using a (probably
hardware-supported) stack on any platform that has reasonable support for
such a thing, which most do). When a function returns, the invocation
record for that function is popped off the stack, local variables for
that invocation record are deallocated appropriately, and control is
passed back to the caller of the function.
Return values are made available to the caller; as with function
arguments, reference returns are given the information needed to find
the object a reference to is returned, and non-reference returns are
given a copy of the object returned. (This is why returning a reference
(or pointer) to a local variable is a Bad Thing - the object that the
reference actually refers to no longer exists.)
Abnormal returns (f'rexample, by throwing an exception) are handled
similarly, except that the object thrown is what gets passed back,
and activation records keep getting removed from the call stack (and
deallocated appropriately - f'rexample, destructors for local objects
will be run) until it gets to an invocation record where that exception
is caught, and control is passed to the catch code rather than to the
point from which the function represented by the last activation record
was called.
Now that you have some idea what you *can* count on, on to more concrete
stuff that depends on the implementation.
Keep in mind that knowing how your compiler does this tells you
nothing useful about how the language requires it to work or how
another compiler (or even your compiler invoked with a different
configuration) works.
(Excuse me for a minute while I go have a chat with the sigmonster...
there, that's better.)
When a function is called, is the stack pointer incremented by the size of
the variables declared in the called function, and then the function will
know where these are in memory relative to the current stack pointer
position.
Most hardware makes it more natural to use a downward-growing stack,
so you're more likely to see the stack pointer decremented on function
entry and incremented on exit (and not at all unlikely to see some or
all of your local variables put in registers and not appearing on the
stack at all), but otherwise this is the way things are normally done
on the types of hardware you're probably used to using.
Note that if any local objects have nontrivial constructors (or
destructors), the constructors are run at the point where the variable
is created (and the destructors are run on function exit), so there's
potentially more to this than just reserving space on the stack.
ALso how are variables returned, is that by a set position in the
stack??
More likely in a CPU register, especially for things small enough to fit
there, but it's also possible for space on the stack to be reserved for
the return value.
If you really want to know how your compiler does this, most compilers
have an option to generate assembly code that you can look at to see.
Note (again; can't say it too many times) that this is only useful
if you're curious about how your compiler does things, and shouldn't
be relied on to provide information that's useful to you while you're
writing C++ code, since it could very well be different with the compiler
you'll be using next week or the next version of your current compiler.
ie,
void func1()
{
int a;
}
void func2()
{
int a,b;
char c;
}
will these both execute at exactly the same speed??
These functions as written will both be translated by any self-respecting
optimizing compiler into a single return (or, if you're lucky, inlined
into nothing at all).
More generally, if for one reason or another the compiler can't or
won't keep the values entirely in registers (there are a few reasons
why it might not, but, say, a loop counter that's not used anywhere but
the loop's control statement is probably going to exist entirely in a
register) and the variables don't get initialized (either explicitly or
by running a constructor), the space for all of them will be allocated
on the stack in a single operation. So creating seventeen local int
variables should take no longer than creating one.
But if anything gets initialized, then the compiler needs to generate
code both to allocate space for it and to initialize it. The allocation
will probably be combined with any other allocations, and therefore not
add anything, but the initialization will need to be done separately
for each object (and can potentially be expensive).
dave