Here is the full method that we refactored:
public void ReturnMemory(byte* pointer) { var memoryDataForPointer = GetMemoryDataForPointer(pointer); _freeSegments.AddOrUpdate(memoryDataForPointer.SizeInBytes, x => { var newQueue = new ConcurrentStack<AllocatedMemoryData>(); newQueue.Push(memoryDataForPointer); return newQueue; }, (x, queue) => { queue.Push(memoryDataForPointer); return queue; }); }
And here is the allocation map for this method:
public unsafe void ReturnMemory(byte* pointer) { <>c__DisplayClass9_0 CS$<>8__locals0 = new <>c__DisplayClass9_0(); CS$<>8__locals0.memoryDataForPointer = this.GetMemoryDataForPointer(pointer); this._freeSegments.AddOrUpdate(CS$<>8__locals0.memoryDataForPointer.SizeInBytes,
new Func<int, ConcurrentStack<AllocatedMemoryData>>(CS$<>8__locals0.<ReturnMemory>b__0),
new Func<int, ConcurrentStack<AllocatedMemoryData>, ConcurrentStack<AllocatedMemoryData>>(CS$<>8__locals0.<ReturnMemory>b__1)); }
As you can see, we are actually allocating three objects here. One is the captured variables class generated by the compiler (<>c__DisplayClass9_0) and two delegate instances. We do this regardless of if we need to add or update.
The refactored code looks like this:
public void ReturnMemory(byte* pointer) { var memoryDataForPointer = GetMemoryDataForPointer(pointer); var q = _freeSegments.GetOrAdd(memoryDataForPointer.SizeInBytes, size => new ConcurrentStack<AllocatedMemoryData>()); q.Push(memoryDataForPointer); }
And what actually gets called is:
public unsafe void ReturnMemory(byte* pointer) { Interlocked.Increment(ref this._returnMemoryCalls); AllocatedMemoryData memoryDataForPointer = this.GetMemoryDataForPointer(pointer); if(<>c.<>9__9_0 == null) { <>c.<>9__9_0 = new Func<int, ConcurrentStack<AllocatedMemoryData>>(this.<ReturnMemory>b__9_0); } this._freeSegments.GetOrAdd(memoryDataForPointer.SizeInBytes, <>c.<>9__9_0).Push(memoryDataForPointer); }
The field (<>c.<>9__9_0) is actually a static field, so it is only allocated once. Now we have a zero allocation method.