1

So after learning the .NET core Queue type uses arrays internally and not nodes I thought I'd have a go at writing my own that does.

The strange thing about MyQueue is that when it has a size of 9200 nodes its Length property returns its correct size, but when it has 9300 nodes it throws a StackOverflowException.

I can't for the life of me work out why this is the case.

QueueNode

namespace MyQ.Core
{
    public class MyQueueNode<T> : IMyQueueNode<T>
    {
        private MyQueueNode<T> _nextNode;

        private T _value;

        //..

        public MyQueueNode(T value)
        {
            _value = value;
        }

        public void SetNextNode(ref MyQueueNode<T> nextNode)
        {
            _nextNode = nextNode;
        }

        public MyQueueNode<T> GetNextNode()
        {
            return _nextNode;
        }

        public T GetValue()
        {
            return _value;
        }
    }
}

MyQueue

namespace MyQ.Core
{
    public class MyQueue<T> : IMyQueue<T> where T : class
    {
        private volatile MyQueueNode<T> _headNode, _tailNode;

        //..

        public void Enqueue(T item)
        {
            MyQueueNode<T> newNode = new MyQueueNode<T>(item);

            if (_headNode == null)
            {
                _headNode = newNode;
                _tailNode = _headNode;
            }
            else
            {
                _tailNode.SetNextNode(ref newNode);
                _tailNode = newNode;
            } 
        }

        public T Dequeue()
        {
            if(_headNode == null)
            {
                throw new QueueEmptyException();
            }

            T value = _headNode.GetValue();

            _headNode = _headNode.GetNextNode();

            return value;
        }

        public T Peek()
        {
            if(_headNode == null)
            {
                return null;
            }

            return _headNode.GetValue();
        }

        public long Length => GetLength(_headNode);

        //..

        private long GetLength(MyQueueNode<T> node = null, long level = 0)
        {
            if(node == null)
            {
                return 0;
            }

            level++;

            if (node.GetNextNode() == null)
            {
                return level;
            }

            node = node.GetNextNode();

            return GetLength(node, level);
        }
    }
}

Test program

using System;
using MyQ.Core;
using System.Diagnostics;

namespace MyQ
{
    class Program
    {
        static void Main(string[] args)
        {
            IMyQueue<string> myQ = new MyQueue<string>();

            //..

            myQ.Enqueue("test 1");

            myQ.Enqueue("test 2");

            myQ.Enqueue("test 3");

            //..

            Console.WriteLine($"The length of the queue is: {myQ.Length}");

            //..

            Console.WriteLine("Peek: " + myQ.Peek()); // 1

            //..

            Console.WriteLine("Dequeue: " + myQ.Dequeue()); // 1

            Console.WriteLine("Dequeue: " + myQ.Dequeue()); // 2

            Console.WriteLine($"The length of the queue is: {myQ.Length}");

            Console.WriteLine("Dequeue: " + myQ.Dequeue()); // 3

            //..

            Console.WriteLine("About to test seed a queue. Press a key to start...");

            Console.ReadKey();

            TestSize(9200); // This works fine...

            TestSize(9300); // This one falls over...

            Console.ReadKey();
        }

        static void TestSize(long seedSize)
        {
            Stopwatch sw = new Stopwatch();

            sw.Start();

            IMyQueue<string> queue = new MyQueue<string>();

            for (int i = 0; i < seedSize; i++)
            {
                Console.WriteLine($"Queue {i}");

                queue.Enqueue(i.ToString());
            }

            sw.Stop();

            Console.WriteLine($"Done. Took {sw.Elapsed.TotalSeconds} seconds.");

            Console.WriteLine($"Queue length is: {queue.Length}");
        }
    }
}
Lee
  • 3,869
  • 12
  • 45
  • 65
  • 2
    Well you should be iterating to find the length, not making a recursive call for ever element of the queue. That's just bad. – Matthew Watson Mar 30 '18 at 10:35
  • As @MatthewWatson said - use iteration instead of recursion. The recursion will blow up your stack at some point. Even if C# had tail calls, the way you write the recursion would not be tail call material. – BitTickler Mar 30 '18 at 10:37
  • The reason I used recursion instead was as a practice exercise as I'd never used it before. Can you both explain why this isn't appropriate? Why does this work for some but not the additional 100 nodes? – Lee Mar 30 '18 at 10:41
  • You should be able to work out why you eventually run out of stack space as you increase recursion depth, without any of us explaining... Just think about it for a minute. :) Anyway, it's not appropriate because it uses stack space to the order of O(N) where N is the number of elements in the linked list, and will cause you to run out of stack space at some point. – Matthew Watson Mar 30 '18 at 10:45
  • Each recursive method call consumes some of the stack space. And stack size is not infinite. So after some number of recursive calls - there is no more space left on stack and you have this exception. – Evk Mar 30 '18 at 10:46
  • @MatthewWatson I was working under the assumption that the stack space can be as large as (or close to) my available system memory (16GB), that is why I didn't think recursion would be a problem. If this is not the case, then there's my answer. – Lee Mar 30 '18 at 10:48
  • 1
    Ah right - stack size is *extremely* limited - the default is 4MB for 64-bit processes, and only 1MB for 32-bit processes. – Matthew Watson Mar 30 '18 at 10:52
  • Wow ok. That's surprising. I'll have to go away and read more about this. Thanks for the comments. Someone should put forward an answer so I can accept it. – Lee Mar 30 '18 at 10:53

2 Answers2

3

The default stack size in Windows programs is extremely limited:

  • 1MB for 32-bit processes.
  • 4MB for 64-bit processes.

This was decided many years ago, and affects all processes, not just C# ones.

This is why your process runs out of stack space so quickly.

Also see here for more details.

Note that it's possible to change the stack size used for a process - by using EDITBIN.EXE - but this isn't recommended.

You can also change the stack size for a new Thread, but again this is not recommended. (This doesn't always work for partially trusted code - the stack size parameter will be ignored for partially trusted code if it exceeds the default stack size.)

Microsoft notes the following:

Avoid using this constructor overload. The default stack size used by the Thread(ThreadStart) constructor overload is the recommended stack size for threads. If a thread has memory problems, the most likely cause is programming error, such as infinite recursion.

Also note that you can't easily change the stack size of a Task.

Matthew Watson
  • 104,400
  • 10
  • 158
  • 276
  • 2
    From your answer it sounds like this size is fixed forever and cannot be changed, while that's not so. For example one can easily create new thread with arbitrary stack size. – Evk Mar 30 '18 at 11:17
  • 1
    @Evk That's true, but isn't directly rated to the problem - and it should be avoided!. I'll update the answer, however. – Matthew Watson Mar 30 '18 at 11:50
  • As a rule of thumb, you only want to set *smaller* stack sizes as a form of optimization. For example if you have a huge number of threads and you know (measured) that your code running on those threads can do with a smaller stack size. I personally used this on embedded (native) programs (Windows CE). It would not be a usual thing to do on desktop machines. – BitTickler Apr 04 '18 at 03:55
1

Actually you can simplify (and optimize) your GetLength method. Just track items count on Enqueue and Dequeue. Try this:

public int Length { get; private set; }

public void Enqueue(T item)
{
  ++Length;
  // ...
}

public T Dequeue()
{
  --Length;
  // ...
}
Aleks Andreev
  • 7,016
  • 8
  • 29
  • 37
  • Thanks Aleks. I was going to put this up on code review to see how much could be improved :) – Lee Mar 30 '18 at 11:05