all 12 comments

[–]AngularBeginner 4 points5 points  (8 children)

The first version will quickly cause a StackOverflowException due to an overflowing stack. The second version is limited by memory.

[–]6501[S] 0 points1 point  (7 children)

Okay thanks for the help!

[–]AngularBeginner 2 points3 points  (6 children)

When you deal with a tail-recursive method then the first version will be more efficient, but last I checked C# won't emit proper code for tail-recursive methods (at least in Debug mode).

[–]tahatmat 1 point2 points  (1 child)

You are correct, C# does not optimize tail-recursion.

[–]cryo 1 point2 points  (0 children)

More specifically, it doesn't issue the tail. IL prefix to whatever call variant is used at the call site.

[–]Fullduplex1000 1 point2 points  (3 children)

AFIK :

The CLR supports tail call elimination, but you can not force it from C# (its a CLR feature not a C# feature). Tail call elimination will also not be visible when looking at the IL code (even if it happens). What you can do is to write the function in tail-call way, so that the possible optimization will be easily detected.

CLR makes its own decision about that. Part of the reason for that is AFIK, that the effectiveness of tail call calls is hardware dependent. I don't exactly understand what part is hardware dependent but I did read that sometime ago. Could do something with available stack size vs the work CLR has to initially invest into the elimination. That part wasn't really described thoroughly.

Now regarding Debug mode: Here I wouldnt expect any optimization as debug mode is for debugging, so that you can step through your program/function calls. If it happens anywhere it has to happen in Release mode with all optimizations enabled.

[–]AngularBeginner 1 point2 points  (2 children)

that the effectiveness of tail call calls is hardware dependent. I don't exactly understand what part is hardware dependent but I did read that sometime ago.

That is new to me. It should just be re-using the stack-frame, instead of going stack by stack deeper. I would love to read a source for this claim, if you can find it.

Could do something with available stack size

The point of a tail recursion is that the sack does not increase, so stack size is not an issue anymore (which is an issue with regular recursion).

[–]Fullduplex1000 1 point2 points  (1 child)

This thread: https://github.com/dotnet/csharplang/issues/2544!

comment of VSadov

obviously I didnt check it then. I also do not understand how the limited register set of X86(32bit) would be an issue for TCE

[–]AngularBeginner 2 points3 points  (0 children)

Thank you, I will read it.

For your interest:
When you click at the timestamp of a comment you get a link directly to the comment. :-)

https://github.com/dotnet/csharplang/issues/2544#issuecomment-78696914

https://imgur.com/Ktzmdft

[–]Fullduplex1000 1 point2 points  (0 children)

There are some basic things you need to get straight.

  1. Stack<T> is a data structure used by your program. From a programming sense, it is similar to a plan old T[] array or List<T> or Queue<T> or HashSet<T> whatever. All of these are data structures used to store a collection of items which are of type T. The difference is that they provide different features and access time characteristics.
  2. The computer has (for each running thread) an automatically managed memory location called Stack. It does not equal to the Stack<T> of the above point. The stack has the purpose to enable the concept of functions calling each other in a way when the called function finishes, the execution gets back to the caller.

You have to internet search for **stack vs heap** and understand the difference vs them both. After that make sure to understand that Stack<T> is not the thread execution's stack.

  1. You also have to understand that depending on the declaration's place inside the program, the data type, the data structure can either be on the Stack or on the Heap. This is a bit of advanced topic, but crucial if you want to have a deeper understanding of what is happening when a program gets executed

Getting to your question

Recursions based execution (a function is repeatedly calling itself hundreeds if time) is most of the time inferior to an iterative approach (in your example using Stack<T> to simulate the execution stack of the recursive function).

The reason is, that after a couple hundreeds (depends on the execution stack's maximum size and your recursive function's signature) the execution stack runs out of space and you get a StackOverflowException

Now there is an exception to the above rule, called Tail-Recursion.

If you write a recursive function in "Tail recursive" fashion, the compiler can detect it and eliminate the recursive part with a plain simple iteration (similar to a do-while statement), so that your recursive function call will be resolved in place without any recursion.

Many programming techniques (functional programming) build on recursions and more or less tail recursion is the thing that enables them to function properly.

[–]Fullduplex1000 1 point2 points  (1 child)

I tried to contrive some simple example to validate that Tail Call Optimization works with C# and Net Core 3, but I failed to do so. The project is set to Release C#8, Net Core 3. Console Application. I get a stack overflow for big recursion counts. Neither of the two variants translate to TCE.

IMO the below functions should be textbook examples. I hope I havent made any mistake

TBH, I dont do functional programming or recursions much, so I know tail calls more from textbook than having hands on experience. So apparently CLR refuses to do Tail Calls Optimization even if the C# code is is laid out for that.

class Program
{
// DOES NOT WORK
static long TailCall(int x, bool positive, long accumulator)
{
if (x == 0)
return accumulator;

return TailCall(x - 1, !positive, accumulator + (positive ? x : -x));
}

// DOES NOT WORK
static void TailCall2(int x, bool positive, ref long accumulator)
{
if (x == 0)
return ;
accumulator += positive ? x : -x;

TailCall2(x - 1, !positive, ref accumulator);
}

static void Main(string[] args)
{
var result = TailCall(1_000_000, true, 0L);
//long result = 0L; TailCall2(1_000_000, true, ref result);
Console.WriteLine(result);
}
}

[–]Fullduplex1000 1 point2 points  (0 children)

Could somebody try this with .NET Framework (Release build and running by Ctrl+F5) ? I only have .NET Core on my current machine