Question

When writing a function like factorial:

fac(Val) when is_integer(Val)->
    Visit = fun (X, _F) when X < 2 -> 
                    1;
                (X, F) ->
                    X * F(X -1, F)
            end,
    Visit(Val, Visit).

one cannot help but notice that tail call optimization is not straight forward however writing it in continuation parsing style is:

fac_cps(Val) when is_integer(Val)->
    Visit = fun (X, _F, K) when X < 2 -> 
                    K (1);
                (X, F, K) ->
                    F(X-1, F, fun (Y) -> K(X * Y) end)
            end,
    Visit(Val, Visit, fun (X) -> X end).

Or perhaps even defunctionalized:

fac_cps_def_lambdas({lam0}, X) ->
    X;
fac_cps_def_lambdas({lam1, X, K}, Y) ->
    fac_cps_def_lambdas(K, X*Y).


fac_cps_def(X) when is_integer(X) ->
    fac_cps_def(X, {lam0}).

fac_cps_def(X, K) when X < 2 -> 
    fac_cps_def_lambdas(K,1);
fac_cps_def(X, K) ->
    fac_cps_def(X-1, {lam1, X, K}).

Timing these three implementations I found that execution time is, as expected, the same.

My question is, is there a way to get more detailed knowledge than this? How do I for instance get the memory usage of executing the function - am I avoiding any stack memory at all?

What are the standart tools for inspecting these sorts of things?

The questions are again, how do I mesure the stack heights of the functions, how do I determine the memory usage of a function call on each of them, and finally, which one is best?

Was it helpful?

Solution

My solution is to just inspect the code with my eyes. Over time, you learn to spot if the code is in tail-call style. Usually, I don't care too much about it, unless I know the size of the structure passing through that code to be huge.

It is just by intuition for me. You can inspect the stack size of a process with erlang:process_info/2. You can inspect the runtime with fprof. But I only do it as a last resort fix.

OTHER TIPS

This doesn't answer your question, but why have you written the code like that? It is not very Erlangy. You generally don't use an explicit CPS unless there is a specific reason for it, it is normally not needed.

As @IGIVECRAPANSWERS says you soon learn to see tail-calls, and there are very few cases where you actually MUST use it.

EDIT: A comment to the comment. No there is no direct way of checking whether the compiler has used LCO or not. It does exactly what you tell it to and assumes you know what you are doing, and why. :-) However, you can be certain that it does when it can, but that it is about it. The only way to check is to look at the stack size of a process to see whether it is growing or not. Unfortunately if you got it wrong in the right place the process can grow very slowly and be hard to detect except over a long period of time.

But again there are very few places where you really need to get the LCO right.

P.S. You use the term LCO (Last Call Optimisation) which is what I learnt way back when. Now, however, "they" seem to use TCO (Tail Call Optimisation) instead. That's progress. :-)

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top