-5

I'm writing a program that is very sensitive to garbage collection so I'm trying to avoid allocating to the heap after initialization.

In the example below, calling Run() will allocate to the heap when it calls DoThing() on any of the elements in the list. This is due to things being backed by structs (Thing1, Thing2, etc).

I want to avoid this allocation, but I also don't want to have to specify multiple lists (one per struct type). Is there a way to do this cleanly and efficiently?

interface IThing
{
  void DoThing();
}

struct Thing1 : IThing
{
  public void DoThing(){}
}

struct Thing2 : IThing
{
  public void DoThing(){}
}

List<IThing> things;

void Initialize()
{
  things = new List<IThing>();

  things.Add(new Thing1());
  things.Add(new Thing2());
}

void Run()
{
  foreach(IThing thing in things)
  {
    thing.DoThing(); //this allocates to the heap due underlying type being a struct 
  }
}

You can see in the image below that there are memory allocations from Line 38. enter image description here

Another example with memory allocations on Line 43. enter image description here

17
  • 7
    The heap allocation actually happens when you add things to the list, as this is when the structs get boxed. There is no going around it: converting any struct instance to one of its interface types will box it. Commented Aug 27 at 23:05
  • 1
    What mutable state does the structs have? If you are only using them for calling interface methods, they can be readonly structs right? Commented Aug 27 at 23:29
  • 2
    Internally a List<Thing> will be backed by an array of structures. This array would still be stored on the heap. Though it would be a contiguous block of memory. The only way to avoid that is to stackalloc a Span<Thing>. Commented Aug 28 at 1:53
  • 7
    why are these types structs in the first place? When you add them to lists and you're afraid of allocations, which is by boxing them using any of their interfaces, then use a datastructure that doesn't have the notion of boxing - namely a class. Commented Aug 28 at 5:32
  • 2
    Thank you for the clarification. Then nothing is allocated in the loop. I don't know what you found related to the memory usage, but keep in mind that most methods of tracking heap memory usage are not reliable and cannot be taken into account is such a simple way. This is a big separate topic. Just don't trust them. Commented Aug 28 at 13:08

2 Answers 2

1

The short answer is "you can't" - however, as with most things there's a huge dose of "well actually" that can be employed in some niche edge cases, for example if the list only ever contains some structurally identical types - for example Memory<T> and ReadOnlyMemory<T>; in that very small family of cases, there is some evil you can apply to discriminate and "pun" at runtime, instead of using naked polymorphism.

Sign up to request clarification or add additional context in comments.

Comments

0

It seems I was mistaken and it does not allocate when calling the interface method.

using System;
using System.Collections.Generic;

interface IThing
{
    void DoThing();
}

struct ThingStruct : IThing
{
    public void DoThing() { }
}

class ThingClass : IThing
{
    public void DoThing() { }
}

class Program
{
    static void Main()
    {
        var thingsWithStructs = new List<IThing>();
        var thingsWithClasses = new List<IThing>();

        // Measure boxing when adding structs
        long before = GC.GetAllocatedBytesForCurrentThread();
        for (int i = 0; i < 1000; i++)
        {
            thingsWithStructs.Add(new ThingStruct()); // boxing happens here
        }
        long after = GC.GetAllocatedBytesForCurrentThread();
        Console.WriteLine($"Adding structs: {after - before} bytes allocated");

        // Measure adding classes
        before = GC.GetAllocatedBytesForCurrentThread();
        for (int i = 0; i < 1000; i++)
        {
            thingsWithClasses.Add(new ThingClass()); // no boxing
        }
        after = GC.GetAllocatedBytesForCurrentThread();
        Console.WriteLine($"Adding classes: {after - before} bytes allocated");

        // Measure calling methods on boxed structs
        before = GC.GetAllocatedBytesForCurrentThread();
        foreach (var thing in thingsWithStructs)
        {
            thing.DoThing(); // no allocation
        }
        after = GC.GetAllocatedBytesForCurrentThread();
        Console.WriteLine($"Calling DoThing() on structs: {after - before} bytes allocated");

        // Measure calling methods on classes
        before = GC.GetAllocatedBytesForCurrentThread();
        foreach (var thing in thingsWithClasses)
        {
            thing.DoThing(); // no allocation
        }
        after = GC.GetAllocatedBytesForCurrentThread();
        Console.WriteLine($"Calling DoThing() on classes: {after - before} bytes allocated");
    }
}

Output:

Adding structs: 40568 bytes allocated
Adding classes: 40568 bytes allocated
Calling DoThing() on structs: 0 bytes allocated
Calling DoThing() on classes: 0 bytes allocated

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.