0

When I run the following code in a unit test, I see that private bytes and working set slowly increase in Performance Monitor and unmanaged memory slowly rises in dotMemory. I've decompiled the source in dotPeek and can't seem to find anything out of the ordinary. Any ideas? Here's the code:

[TestMethod]
    public void TestEnumeratorMoveNext()
    {
        //Dictionary<string, int>[] outDict = new Dictionary<string, int>[32].Select(x => x = new int[100].ToDictionary(y => Guid.NewGuid().ToString())).ToArray();
        var outDict = new Dictionary<string, int>[32];
        for (int j = 0; j < 32; j++)
        {
            outDict[j] = new Dictionary<string, int>();
            for(int q = 0; q < 100; q++)
            {
                outDict[j].Add(Guid.NewGuid().ToString(), 0);
            }
        }

        for (int i = 0; i < 10000; i++)
        {
            var enumerator = new Enumerator<string, int>(outDict);

            while (enumerator.MoveNext()) { }

            Thread.Sleep(1000 / 60);
        }
    }

    public struct Enumerator<TKey, TValue> : IEnumerator<KeyValuePair<TKey, TValue>>
    {
        private Dictionary<TKey, TValue>[] dictionary;

        private int partition;
        private IEnumerator<KeyValuePair<TKey, TValue>> enumerator;
        private bool moveNext;

        private int totalPartitions;

        internal Enumerator(Dictionary<TKey, TValue>[] dictionary)
        {
            this.dictionary = dictionary;
            this.totalPartitions = dictionary.Count();

            partition = 0;
            enumerator = new Dictionary<TKey, TValue>.Enumerator();
            moveNext = false;
        }

        public bool MoveNext()
        {

            if (partition < totalPartitions)
            {
                do
                {
                    var outDict = dictionary[partition];

                    if (!moveNext)
                        enumerator = outDict.GetEnumerator();

                    moveNext = enumerator.MoveNext();

                    if (!moveNext)
                    {
                        enumerator.Dispose();
                        partition++;
                    }
                } while (!moveNext && partition < totalPartitions);

                return true;
            }

            partition = totalPartitions + 1;
            enumerator = new Dictionary<TKey, TValue>.Enumerator();
            moveNext = false;
            return false;
        }

        public KeyValuePair<TKey, TValue> Current
        {
            get
            {
                return enumerator.Current;
            }
        }

        public void Dispose()
        {
            enumerator.Dispose();
        }

        object IEnumerator.Current
        {
            get
            {
                return new KeyValuePair<TKey, TValue>(Current.Key, Current.Value);
            }
        }

        public void Reset()
        {
            throw new NotSupportedException();
        }
    }

I appreciate any help anyone can give me. Thanks in advance.

Meadock
  • 109
  • 1
  • 1
  • 7
  • Your `object IEnumerator.Current` is creating a **new** kvp every time it's called while public implementation just returns `enumerator.Current;`. I'm not sure if this is causing problems but the IEnumerator implementation looks kind of strange. – bokibeg Mar 14 '15 at 21:28
  • 1
    I doubt the bytes increase forever. Why is there a sleep? – usr Mar 14 '15 at 21:36
  • 2
    The problem is *slowly*, it takes until Christmas before the GC runs. Delete the Thread.Sleep() and run the method a thousand times. Now you see the sawtooth pattern. – Hans Passant Mar 14 '15 at 21:38
  • @bokibeg That really shouldn't be the problem, as `KeyValuePair` is a struct type. Simply returning `Current` would be more obvious, but also involves boxing of the return value. The `Current.Key/Value` invocations should be purely a stack thing and not affect the heap in any way. – Wormbo Mar 14 '15 at 22:03
  • In other words you're probably wrong when you say that it is _unmanaged_ memory that is increasing. – RenniePet Mar 14 '15 at 22:17
  • @usr I have the sleep in there to slow things down a bit for performance monitor. As Hans Passant says, you can remove the Thread.Sleep and just run that for loop 1000 times to see things run faster (and uglier). When I look at that in dotMemory, it definitely makes things a little more obvious. There's definitely a sawtooth pattern and I only see unmanaged memory freaking out. The problem is that it's causing a lot of gen0 collections and I can't have that happening. – Meadock Mar 14 '15 at 22:38
  • One more thing that I've noticed, and I don't know if this could be the problem, but there's a ConcurrentStack in something called PinnableBufferCache that's creating a bunch of new nodes. The callstack according to dotMemory looks something like this: Gen2GcCallback.Finalize() -> PinnableBufferCache.Gen2GcCallbackFunc(Object targetObj) -> PinnableBufferCache.TrimFreeListIfNeeded() -> PinnableBufferCache.AgePendingBuffers() -> ConcurrentStack.Push(T item) – Meadock Mar 14 '15 at 22:44
  • You can't have G0 collections? Then you cannot allocate anything at all. – usr Mar 14 '15 at 23:04
  • @usr the couple of garbage collections that occur on startup are fine. It's garbage collections occurring over the course of that second for loop that I can't have. – Meadock Mar 14 '15 at 23:14
  • @HansPassant any idea what's causing that sawtooth pattern? – Meadock Mar 14 '15 at 23:15
  • 1
    That's the GC doing its job. Avoid using a memory profiler without knowing how the garbage collector works, you can't make any sense of what you see. – Hans Passant Mar 14 '15 at 23:26
  • Either stop allocating or put the GC into low latency mode during the critical period. – usr Mar 14 '15 at 23:41
  • @HansPassant let me rephrase my question: "Any idea what is causing the garbage collections?" I realize it's an odd thing to say that "unmanaged memory is causing garbage collections to occur," so it would have been better to say that there's a correlation between unmanaged memory increases/decreases and garbage collections, or at least that's what I"m seeing. In any case, I believe I've found a solution to my issue, but I'm still not sure what was causing it in the first place. I'll post the answer shortly. t4tm. – Meadock Mar 15 '15 at 01:14
  • I _still_ think that you're talking about _managed_ memory, not _unmanaged_ memory. – RenniePet Mar 15 '15 at 04:38
  • @RenniePet After speeding things up as Hans Passant suggested, I did notice that Bytes in all Heaps was being affected. And based on the solution I came up with, it seems to me that there was some sort of boxing going on, so that would explain the heap changes and GCs. But one thing that I still can't explain or don't understand is why dotMemory was showing that the unmanaged memory was rising and falling with GCs. Thanks for pointing this out, though. – Meadock Mar 15 '15 at 19:46
  • Get snapshot after a time and look at memory traffic. dotMemory shows you what objects are created and where, and you will not have to guess – Ed Pavlov Mar 16 '15 at 20:31

1 Answers1

0

The solution is to change private IEnumerator<KeyValuePair<TKey, TValue>> enumerator to private Dictionary<TKey, TValue>.Enumerator enumerator in the Enumerator struct. No more G0 GCs. If anyone knows why this is the case, I would love to hear the explanation.

Meadock
  • 109
  • 1
  • 1
  • 7
  • dotPeek shows that there's a cast on outDict.GetEnumerator() from Dictionary.Enumerator to IEnumerator>. Is there some sort of boxing going on? `if (!this.moveNext) this.enumerator = (IEnumerator>) dictionary.GetEnumerator();` – Meadock Mar 15 '15 at 01:25
  • Dictionary.Enumerator is a struct. As I said: Stop allocating if you want to avoid G0's. Maybe your question should have been "I can't allocate at all in this piece of code. Where is the allocation?". Many people could have answered that. – usr Mar 15 '15 at 09:25
  • @usr Correct me if I'm wrong, but creating structs does not cause GCs. Structs are managed on the stack, not the heap. The GC manages the heap, not the stack. – Meadock Mar 15 '15 at 19:49
  • Then I must have misunderstood you. I though you said that using a struct removed the occurrence of GCs. This is expected as you now say. But in this answer you say that you don't understand this observation. – usr Mar 15 '15 at 19:50