Delegates, events, and garbage

Delegates, events, and garbage

  • Comments 6

Some commenters on my previous post asked for more information about the situations where delegates allocate memory. Here's the scoop.

There are actually three reasons why careless use of delegates or events can cause memory allocations:

 

Allocating delegate instances

Consider this test class:

    class DelegateTest
    {
        void TestMethod()
        {
            PassIntegerToDelegate(PrintInteger);
        }

        void PrintInteger(int value)
        {
            Trace.WriteLine(value);
        }

        void PassIntegerToDelegate(Action<int> delegateInstance)
        {
            delegateInstance(23);
        }
    }

No allocations there, right?

Bzzzt! Wrong.

Loading the compiled test assembly into Reflector (which, by the way, is a truly invaluable tool for .NET development. If you don't already have it, go download it straight away!) shows what the compiler did with our TestMethod:

    private void TestMethod()
    {
        this.PassIntegerToDelegate(new Action<int>(this.PrintInteger));
    }

It is actually impossible to construct a new delegate without allocating memory, because delegates are reference type objects that live on the heap. Unlike raw function pointers in C, delegates are more than just a pointer to some code. They also store a 'this' reference, providing context for how that code should be invoked. That is why you can create delegates from instance methods, unlike C++ where you can only obtain function pointers to static methods. Some heap space is required to store the association between method and 'this' instance.

The compiler (as of C# 2.0, anyway: you used to have to do this by hand) hides the memory allocation via syntactic sugar that automatically constructs delegates whenever you pass a raw method somewhere a delegate is expected, so you rarely need an explicit "new" statement in your code, but the allocation is still there behind the scenes.

One technique for avoiding garbage is to manually cache the delegate instance so you only have to create it once. This can help if you have a method that will be called many times, and will use the same delegate binding each time:

    class DelegateTest
    {
        Action<int> cachedDelegate;

        void TestMethod()
        {
            if (cachedDelegate == null)
                cachedDelegate = PrintInteger;

            PassIntegerToDelegate(cachedDelegate);
        }
        ...

Events are actually just syntactic sugar over the top of delegates, so they have the same allocation behavior. This test class:

    class EventTest
    {
        event EventHandler MyEvent;

        void SubscribeToEvent()
        {
            MyEvent += MyEventHandler;
        }

        void MyEventHandler(object sender, EventArgs e)
        {
        }
    }

Produces this when examined in Reflector:

    private void SubscribeToEvent()
    {
        this.MyEvent = (EventHandler) Delegate.Combine(this.MyEvent, new EventHandler(this.MyEventHandler));
    }

I am not aware of any way to subscribe to an event without causing memory allocations. Raising events does not necessarily allocate, though, so you'll be fine as long as you can hook everything up ahead of time while loading your levels.

 

Allocating lexical closures

If you are a fan of functional programming like me, you will love the anonymous delegate syntax that was introduced in C# 2.0. It is getting even nicer in C# 3.0. Hurrah!

Unfortunately, however, anonymous delegates can be a major cause of hidden allocations. Compile this test class:

    class ClosureTest
    {
        void TestMethod(int valueOffset)
        {
            PassIntegerToDelegate(delegate(int value)
            {
                Trace.WriteLine(value + valueOffset);
            });
        }

        void PassIntegerToDelegate(Action<int> delegateInstance)
        {
            delegateInstance(23);
        }
    }

And look at the output in Reflector:

    internal class ClosureTest
    {
        [CompilerGenerated]
        private sealed class <>c__DisplayClass1
        {
            public int valueOffset;

            public void <TestMethod>b__0(int value)
            {
                Trace.WriteLine(value + this.valueOffset);
        }
        private void TestMethod(int valueOffset)
        {
            <>c__DisplayClass1 <>8__locals2 = new <>c__DisplayClass1();
            <>8__locals2.valueOffset = valueOffset;
            this.PassIntegerToDelegate(new Action<int>(<>8__locals2.<TestMethod>b__0));
        }
        private void PassIntegerToDelegate(Action<int> delegateInstance)
        {
            delegateInstance(0x17);
        }
    }

In order to preserve our valueOffset parameter so the anonymous delegate can close over it, the compiler has created an internal helper class, allocated on the heap, which can be shared between the TestMethod body and the delegate implementation code. Clever compiler! But not good if we are trying to avoid allocations.

Note that anonymous delegates come in two flavors. Free standing ones (which do not reference any variables from their parent scope) can be implemented as simple static methods, and will not cause any more allocations than any other kind of delegate. It is only when an anonymous delegate is used as a lexical closure that the compiler will generate this extra hidden allocation.

 

Allocating EventArgs instances

The .NET Framework Design Guidelines are very specific about how events should be implemented. Unfortunately this is one of the (thankfully rare) cases where the word of the guidelines is in conflict with the demands of efficiency.

The guidelines say all event handlers should take an argument parameter, which should be of a type derived from System.EventArgs. Trouble is, System.EventArgs is a reference type, so this causes an allocation every time you want to raise an event.

You could keep around a single argument instance, and reuse it from one event to the next. That seems a little odd, though. What if the event handler stores the argument instance somewhere, and wants to come back and look at it later? That seems like a reasonable thing for them to do, but they would get unexpectedly wrong results if you later changed the contents of this instance as part of raising some other unrelated event.

You could just ignore the design guidelines, and make your events without any argument parameter, or perhaps using a value type parameter. Or you could follow the guidelines, and put up with the garbage. Or you could dodge the issue by avoiding events altogether in your main gameplay code.

  • RE: Allocating EventArgs...

    Isn't this why  EventArgs.Empty exists?

    -Brandon Bloom

  • EventArgs.Empty is good if you have no arguments to pass, but won't help if you do actually have a custom argument type and want to put some real data in there. I don't have a good feeling for how many events have no args, but it's maybe 50/50?

  • Thanks for elaborating on the event/delegate issue - passing structs by reference is working fine for me, and will do until XNA supports a generational GC (come on Shawn, you *know* you need one! ;)

  •            PassIntegerToDelegate(delegate(int value)

               {

                   Trace.WriteLine(value + valueOffset);

               });

    I don't understand the placement of the braces and ");".

  • 8o8: that's a C# anonymous delegate.  See msdn.microsoft.com/.../0yw3tz5k(v=VS.100).aspx

  • I know this is an old post, but I'd just like to share that it is possible to write your add methods for your own events which actually add the delegate to a preallocated collection, potentially eliminating allocation if subscribing to events during tight loops. A favorite trick of mine is to use a custom linked list of delegates with a static pool of nodes which I can share between different instances. I know this kills cache coherency, but there are times where this is preferable to having the garbage collector pause my app.

Page 1 of 1 (6 items)
Leave a Comment
  • Please add 4 and 5 and type the answer here:
  • Post