How to manage different delegate signatures/Types without switch/case (GC-free)

(apologies for the long post, but i hope to illustrate some points)
My aim is to create garbage-free Delegate Manager with support for any number of delegate signatures/types.

These forums helped me a lot in making that happen. Basically im using a list for delegates, which only register other single-callback delegates, to avoid garbage. Im also using an ObjectPool to pre-allocate the delegate instances, so that there is no garbage when adding/removing/invoking delegates during runtime.

The problem i have, is when i try to automatically handle different delegate types. I tried generics, but the delegate must be strongly typed during declaration, also there is no base class for a delegate. And then there is an issue of Invoking a delegate using generics.

From what i can see (and i hope to be wrong), there are only 2 ways to handle the storage(structure)/addition/removal/invocation of managed delegates, that would be garbage free. There are drawbacks to each of these and i hope someone can offer alternative or suggest some C# magic.

[i will now post simplified version of structure, to illustrate these points]

Option1: ā€œstore by typeā€, as an object.
With a Dictionary<Type, List>, i can store by delegate Type, and store the instance as an object. This messes up OOP which i dont like, but could live with here, if it could automatize the management of different delegate types. Storage and removal thus works for any delegate type. Execution is a problem.

Example (Execution):

// invoke registrees of specific type (note this is minimal code sample, and doesnt make sense on its own. Just focus on the responsibility, which is Invocation of a delegate type.)
public void InvokeListeners<T>()
{
   Type registreeType = typeof(T);
   List<object> registrees = registreeDelegates[registreeType];
   for (int i=registrees.Count-1; i>=0; i--) {
       //registrees[i]();                        // cant invoke an object. how to tell it its a delegate of its Type?
       //((VoidVoidDelegate)registrees[i])();   // works, but assumes a Type.
       //((T)registrees[i])();                   // in case of generic function, we cant assume T is invokable
       ExecuteAsDelegate (registrees[i], registreeType);   // switch/case works, but breaks automatization
   }
}

private void ExecuteAsDelegate (object registree, Type registreeType)
{
    // how to automatize this, not have to call by specific type
    // afaik there is no way to Invoke a generic delegate (cast as object),
    // while knowing only its System.Type?
    if (registreeType == typeof(VoidVoidDelegate)) {
        ((VoidVoidDelegate)registree)();
    }
    // else .. for remaining delegate Types
}

Option2: Using overloaded functions, when adding different delegate Types.
Good OOP, bad for management (adding almost duplicate code, every time we add new delegate signature).
Structure changes. Instead of Dictionary<Type, List>, we use separate variables for typed Lists (also bad for management).

Example (Add Listener):

public void AddListener(VoidVoidDelegate registree)
{
   if (!registreesVoidVoid.Contains(registree)) {
       registreesVoidVoid.Add (registree);
   }
}
public void AddListener(StringVoidDelegate registree)
{
   if (!registreesStringVoid.Contains(registree)) {
       registreesStringVoid.Add (registree);
   }
}
public

TL;DR:
Option1: bad for OOP, provides automatization, except for Invocation, which i dont know how to automatize.
Option2: good for OOP, separate variables for list of each delegate type, extra functions per each type = bloated code.
Option3: Your suggestions/ammends for a better approach than Option1/2. Requirements are 1) full automatization (just call by type and manager handles the rest) and 2) garbage-free. OOP-friendly (no ā€˜objectā€™) is nice but not required for this sealed class.

I have some other thoughts on the design in general, but what about this ā€œC# magicā€

public void InvokeListeners<T>() where T : Action
{
   Type registreeType = typeof(T);
   List<object> registrees = registreeDelegates[registreeType];
   for (int i=registrees.Count-1; i>=0; i--) {
       ((T)registrees[i])();
   }
}
1 Like

Tried something similar before. But this throws:
Error CS0701 'Action' is not a valid constraint. A type used as a constraint must be an interface, a non-sealed class or a type parameter.
Not gonna lie, ive learned something new myself there, when i tried it. delegate is the same case. Its not a non-sealed class, interface or type (it does not regard delegate/Action to be a type). EDIT: Infact i see now that Action is a delegate. Then i tried to look for having my own base class for custom delegates and using that, but that went too far, although it might work as an alternative.

Mind sharing you idea of different approach to design in general? Some pattern i could check out somewhere perhaps?
Im all in for learning new things, as i donā€™t come from .NET background, and it always helps to know more.

Huh. Thatā€™s new to me too.
I think Action and other delegates are still types ā€“ they just canā€™t be used as a constraint. It actually makes sense as you dig into the problem, since a specific type of delegate canā€™t be extended, so you may as well make the argument an Action or delegate directly and not bother with constraints.

It also doesnā€™t make much sense to use a specific type of delegate as a key in a dictionary for the same reason: specific delegates cannot be inherited. There is no compatibility between a delegate that takes no parameters and one that takes 10 parameters. Likewise, there is no compatibility between a delegate that takes an int and another that takes a string. If you want to differentiate between types of delegates, then you have to use what makes them different: their return types and their list of parameters. You could use a collection of types as the key.

What I meant by ā€œother thoughtsā€ is that it sounds like you are either: a) prematurely optimizing or b) building a framework.

If youā€™re trying to optimize before knowing the garbage generated by your delegates is a performance bottleneck, stop. Build the game first and then use the profiler to see if this is an actual problem. I bet itā€™s not.

If youā€™re trying to build a framework, you need to explain a bit more clearly what that framework is supposed to do. Maybe give an example. As it was written, your code could only hope to handle delegates with no parameters. You might get the storage of arbitrary delegates to work, but when it is time to invoke them, where are your parameters going to come from? Itā€™s typically the caller of the delegate that has the parameters, not some managing framework. And if you have to end up passing those parameters to the framework, then what is the framework actually buying you?

1 Like

@eisenpony : Thank you, i appreciate all you wrote.
I was able to use your ā€˜Actionā€™ hint, to call it like this:

T myDelegate;
(myDelegate as Action)();

Which compiles, the problem then is, i need to support (manager) ā€˜anyā€™ delegate signature, and Action is void-void only. But Func seems to cover what i need.

What you said about premature optimization - this is very true. I am an extremist, in a sense of an extreme pedant. Its blessing and a curse. I very much like the idea of using best practise/having clean code to build on top of (and yes, this manager is used in many places, and i want it to be solid), and understanding the nitty-gritty of structure, logic, optimizationā€¦ Its not only process of development, but also of constant learning. It probably is in any case, and what i am describing is my personal bias. I often profile everything and play with the idea of 0-garbage policy, and my favorite game dev videos would have to be things like playdeadā€™s inside presentation (this was before Unityā€™s incremental GC was added). During the nerd hype its easy for me to loose the big picture, and i secretly admire the people that are able to stay above and keep their cool).

Cheers!

I can relate. I love the technical challenge of something pure and often get lost in it. Personal preference definitely plays a role here but keeping things clean a pure like this takes a great amount of skill and tenacity, so kudos to you. However, keeping deadlines and producing business value is also a skill, and one that you must practice if you intend on improving your value to a company. For me, it takes great will power to make a fix ā€œgood enoughā€ and then move on, but, unless youā€™re on a solely personal edification adventure, it is a necessary evil.

Back to the topic at hand ā€¦ have you thought about my last paragraph?

Itā€™s still not clear to me what you are trying to accomplish. Can you show how a consumer of your code would use it?

Very true, and as you said, its not only about personal ā€˜preferenceā€™, or it could become one, if processed appropriately, as a personal challenge to overcome ones biggest deficits. Moreover, i treat this as a personal hobby, not business. The ā€˜0 gc policyā€™ is less of an issue now, since incremental compiler and other features made the GC spikes much less of an issue.

Iā€™ve actually read it, but when it came to reply, completely forgot to address it.

I was only interested in inputs, not return values. Dispatcher would tell the framework to dispatch to all Listeners with appropriate input parameters. Framework has the list of all listeners (the functions wrapped in delegates) and it invokes them. Listener registers to framework by Dispatcherā€™s id and ā€˜event typeā€™, which are both ints defined by Dispatcher. Framework Invokes all Listeners callbacks (delegates), registered to specific Dispatcher to specific ā€˜event typeā€™. The actual responsibility of the manager is registration, invocation and removal of delegates gc-free, this was the first purpose at least. But as you mentioned this is only possible for void-void delegate signatures, so the parameters have to be stored elsewhere. For VoidVoidDelegate signatures this works very well, and i will probably keep it that way and have a way to deliver the parameters differently.

Iā€™m sure you know this, but gc imprint rises with number of functions registered to a delegate. The reason afaik is that internally, MulticastDelegate is created that grows the more functions are registered, and shrinks as they are unregistered. When you invoke such delegate, with 1 or more registered functions, thatā€™s a gc hit (the cost is linear and depends on number of functions registered).

private delegate void VoidVoidDelegate ();
private VoidVoidDelegate voidVoidDelegate;
..
voidVoidDelegate = MyFunction;

However, if you register a function wrapped in a cached delegate, the assignment, removal and invocation are all free:

voidVoidDelegate = myFunctionAsVoidVoidDelegate

But if you register 2 functions to myFunctionAsVoidVoidDelegate, there is a slight hit again for registration/invocation/unregistration.

So the manager basically creates the lists for you, and you register/invoke/unregister via the manager, and it manages the structures appropriately (it works with a global instance pool etc) - to achieve 100% garbage free VoidVoid delegate signature support. Inside manager, each item in the List represents one function registration wrapped as one delegate. The whole list then represents what amounts to one MulticastDelegate, the manager just calls each single List item in a loop during invocation, where you would normally call myMulticastDelegate().

At some point however i needed a parameter to be passed, so i started thinking how to integrate different delegate signatures, either that, or keep the input data in the Dispatcher. Atm dispatcher doesnt know about listeners, but listeners do interface with Dispatcher (they need its IDs to register to). However the Dispatcher can still (when invoking through the manager) send input data, which would be passed into all invoked delegates as input parameter. And since i invoke by type, i know what data to send (i mean that its strongly typed, so even Dispatcher has to adhere to the signature, which is good).

I did not know this. Iā€™m not usually pursuing garbage this aggressively though. Very surprising that invoking delegates creates garbage.

So it sounds like you have this working for the parameterless scenario. Letā€™s try taking a small step forward and add one parameter.

Rather than tracking the delegate type, I think you need to track each of the parameter types. Iā€™m thinking something like :

public class Dispatcher
{
  private Dictionary<Type, ICollection<object>> Listeners { get; set; }
  private ICollection<object> GetListeners(Type t)
  {
    if (!Listeners.ContainsKey(t)
      Listeners[t] = new List<object>();

    return Listeners[t];
  }

  public Dispatcher()
  {
    Listeners = new Dictionary<Type, object>();
  }

  public void AddListener(Action registree)
  {
    GetListeners(typeof(void)).Add(registree);
  }

  public void AddListener<T>(Action<T> registree)
  {
    GetListeners(typeof(T)).Add(registree);
  }

  public void InvokeListeners()
  {
    foreach (var l in GetListeners(typeof(void)))
      ((Action)l)?.Invoke();
  }

  public void InvokeListeners<T>(T param)
  {
    foreach (var l in GetListeners(typeof(T)))
      ((Action<T>)l)?.Invoke(param);
  }
}
1 Like

Why donā€™t you cache them ā€œin-placeā€, e.g. as a member of the calling type/context?

The problem with such general manager is that youā€™r going to make lots of code depend on it. Probably okay if youā€™re not going to write a flexible code base, but it might still sooner or later make you rage out.

Also, thereā€™s missing a huge piece in the puzzle. Storing ā€œpre-allocatedā€ instances is fine, but what about various different actions that have a very different calling context?
Store an action that is supposed to run method X, store an action that is supposed to run method Y. There needs to be some value or identity to distinguish the context of the desired invocation chain, otherwise you would definitely need one explicit delegate type declaration for every scenario. This was most commonly done when generics, i.e. when generic EventHandlers did not exist, and when general purpose delegates like Action[<>] and Func[<>] werenā€™t available.

Is that really an issue? How often do you subscribe / unsubscribe? There may be better alternatives if thatā€™s a real issue. Yes, you might aim for perfection in regards of that, but the question is if thatā€™s really worth it.

Can you post a concrete example?

System.Object
System.Delegate
System.MulticastDelegate
[ any other delegate type comes next in the hierarchy ]

You could run dynamic invocation via the Delegate / MultiDelegate type. But this is much slower than a direct invocation, as it needs to check various aspects before the actual invocation can take place.

If you only need parameterless delegates like System.Action, you could do the case, like youā€™ve already discovered.

For invalid types passed as the actual T: in those situations in which constraints arenā€™t enough, you should (or rather, have to) type-check the actual T in order to make this behave properly. That is, throw an exception when the type is not of the expected type.

Again it would be helpful to see a concrete example that illustrates real benefits.
Option1 can be improved upon with a little more effort, potentially eliminating the concerns you see in the OOP stuff, though itā€™ll still require type-checking for the sake of completeless and correctness. Itā€™s similar to what @eisenpony has already posted:

Basically something like this, except that I wouldnā€™t start to store the parameter type for the action, but the actual action type (runtime-generated for any concrete generic usage).

Invocation itself does not, unless the methods wrapped in it do so. He probably referred to adding/removing to delegates, as that requires duplication of the entire internal structure (depending on the implementation itā€™s either an array, linked list, ā€¦).

1 Like

_watcher says:

Followed by Suddoha:

I too would like to see a concrete example. Because this to me screams a potential for ballooning memory.

I understand the desire to avoid GC. But at the same time GC is needed to release unused memory.

If you create delegates and cache them for objects that later get destroyed (like say a delegate for a method on a Component, and then you load a new scene). Now that delegate is stuck. You have a reference to it, and it a reference to the component, but the component is destroyed. Now the component and the delegate are both stuck in memory but unusable for anything.

Lets say this scene is a room in your game. As the player goes in and out of the room more and more delegate/component pairs get orphaned and stuck in memory. Filling up your pool, but unusable for anything. And before you know it youā€™re crunching gigs of ram. Which can be an added problem when youā€™ve built for 32-bit and can really only allocate a few gigs of working memory anyways.

I see this at work all the time (business/enterprise level). Right now one of our ā€œcowboyā€ developers (ugh I canā€™t stand him) wrote up this WPF control that creates objects willy nilly that are getting stuck in memory because he leaves references to them laying around. The thing takes up 30 megs every time the form that uses the control is opened. The form that uses it? Oh itā€™s just you knowā€¦ the order form used for every transaction in the ordering software its for! So as the clients day progresses 30 more megs stuck in memory every time they open an order! Something they do 100ā€™s of times a day! By 3pm every day the system crahses ā€œout-of-memoryā€.

ā€¦

Now you might say ā€œI reuse these objects, so pooling them is fine.ā€

But are you pooling all of them? And are you holding on to every single thing pooled?

Lets say you create a little pooled token for something used frequently (like say a delegate). Now lets say this token is single use so if there isnā€™t a token in the pool to recycle, a new one is created. Now lets say during some intense part of your code/game a LOT of work is done and thousands of delegates are neededā€¦ and thusly thousands of tokens are created. The job is done and all of those tokens are released to the pool and they just sit there waiting to be recycled.

Butā€¦ oh, that was the game ā€œloadā€ sequence and itā€™ll never happen again for the rest of the game. So now we have thousands of tokens just sitting around waiting to be reused and never will because on average only 100 tokens are ever in use at any given momentā€¦ it was only during that huge game load sequence that thousands+ were used. Now you just have the amorphous chunk of memory sitting there taking up space, getting in the way of defragmenting the heap the next time a large array is needed, and doing nothing. Like a recycling center full of used plastic grocery bags no one needs so we ship it to China where it sits in giant piles effectively like a dump (wooo, political tangent!).

Speaking ofā€¦ arrays, thereā€™s a big one that pooling can really sneak up on your butt with!

2 Likes

I had to re-read what i wrote, and realized that i lied.

I wrote:
Im also using an ObjectPool to pre-allocate the delegate instances

But im not pooling the delegate instances :sweat_smile:
Im only pooling the associated manager structures that store them. The delegate callback instances are of course in the Listener classes that register them, so we just pass the reference to the manager, so to speakā€¦

This is what is pooled

// instantiate some complex objects that we'll use later
ObjectPool.Instance.CreatePool<Dictionary<int, Dictionary<int,      List<VoidVoidDelegate>>>>(1);
//                                        ^delegatorId    ^eventTypeId   ^delegateType
ObjectPool.Instance.CreatePool<Dictionary<int, List<VoidVoidDelegate>>>(1);
ObjectPool.Instance.CreatePool<List<VoidVoidDelegate>>(1);

// grab some from the pool instead of 'new' instantiating...
registreeDelegates = ObjectPool.Instance.GetObject<Dictionary<int, Dictionary<int, List<VoidVoidDelegate>>>>();

I realize now, i confused the debate from the beginning. Sorry about that. The DelegateManager simply stored single-registree delegates in a convenient structure, so adding/removing and invoking them is garbage-free. Thatā€™s all it does. The one single responsibility for this manager is to achieve gc-free delegate implementation.

Yes, there will be dependency, in a sense that the same like that would register/unregister/invoke a delegate, would now be replaced by line that registers/unregisters/invokes it via a manager. If i canā€™t fallback to using DelegateManager, like for ex. if i canā€™t use a void-void signature delegate, i can just use the default C# delegate.

I really did a number on writing that i pool delegate instancesā€¦ damn (

Not an issue, lets just call it experimentation and learning. Im pretty noob at all this coding and this is the way i learn all about and around it. Yes, i dont think its an issue, but i dont see why not use it. I understand when u say ā€˜aint decoupled now, lost of code now depends on that managerā€™, but for me its just a line of code just like using default delegate add/remove/invoke (until the manager completely changes, which in turn breaks the project HA HA) - but nah, its pretty sealed now. Can use void-void gc free, just like i use void-void not-gc-free using System.Delegate. Its still using System.Delegateā€™s, just the structure (which is - i assume - some internal array getting resized in MulticastDelegate when you register multiple functions/delegates into it, which was causing de/allocations, that is restructured in the manager so no such thing occurs, thus no gc hitā€¦).

Except i was being an idiot saying i pool delegate instances, as i just explained in reply to @Suddoha .

So the fact is, they are just cached in the class that registers them. As it should be. And pool is only used to store the structures that store their references, inside manager. These structures are not instantiated everytime some listeners want to register to an event, they are fetched from the pool. Just the Dictionary<int, Dictionary<int, List> and related structures that are there to hold the delegate references, as i just wrote in the other post.

AYAYAY garambol!

I also have custom profiler for the objectpool, so i can see how much of what is used/unused, and can then decide to manage instantiation in fixed manner per area, or dynamically based on usage, which can also be serialized into a table so as to feed the fixed ā€˜per areaā€™ instantiation scheme. The pool is fine as is, im satisfied with it. But of course me writing i am pooling delegate instances was somewhat of a brainfartā€¦ quess i was thinking of writing that instances are cached in the caller, and also that their structures within manager are cached inside objectpool, and it just melted together coz my hands cant keep up w my mind.

I appreciate all the replies, and wish i could give u some cookies besides the likes x)
Thanks for all the good suggestions, and iā€™ll pursue them later as needed, for now im sticking with the voidvoid delegate signature inside manager, and wont deal with other signatures, until when i have more time to spend on this.

@eisenpony Thanks, thats some good inspiration there!
Ill keep void-void manager for the moment, as i need to work on something else now, but i might try that later.
When you write ((Action)l)?.Invoke();, its the same as ((Action)l)(); yes?
Where can i read up on that syntax, using the ?. invocation? Any link you have to share?

One more correctionā€¦

I posted that following worked:

T myDelegate;
(myDelegate as Action)();

Well it just compiled properly.
It equals Null after the cast.
Some related code, to see what is happening:

List<T> regs = typedCollection[delegatorId][callbackId];
for (int i=regs.Count-1; i>=0; i--) {
    Debug.Log(regs[i]);                     // traces: DelegateManager+VoidVoidDelegate
    Debug.Log((VoidVoidDelegate)regs[i]);   // doesn't compile: Cannot convert Type 'T' to 'DelegateManager.VoidVoidDelegate'
    Debug.Log((Action)regs[i]);             // doesn't compile: Cannot convert Type 'T' to 'System.Action'
    Debug.Log(regs[i] as VoidVoidDelegate); // traces: DelegateManager+VoidVoidDelegate
    Debug.Log(regs[i] as Action);           // traces: Null
    Debug.Log(regs[i] as System.Delegate);  // traces: DelegateManager+VoidVoidDelegate
    ((Action)regs[i])?.Invoke();            // doesn't compile: Cannot convert Type 'T' to 'System.Action'
    (regs[i] as Action)?.Invoke();          // compiles, throws exception: NullReferenceException: Object reference not set to an instance of an object
    (regs[i] as System.Delegate)?.DynamicInvoke(); // works ok; seem you can even pass args
}

So ā€˜asā€™ cast to Action returns Null
And direct cast (Action)regs throws InvalidCastException.
Explanation of those two types of casting is explained in first answer here.
That makes sense, VoidVoidDelegate does not subclass System.Action, but System.Delegate (System.Action is subclass of System.Delegate - thanks @Suddoha ). Base class being System.Delegate, we need to use that. System.Delegate does extend System.Object, but we cant invoke System.Object, so lets just stick with System.Delegate.
If we cast it as System.Delegate instead of System.Action it works properly.
Invoke using DynamicInvoke(params object[ ] args);
Your suggestion using params stored via their Type is appreciated, i should check that later.
Many thanks everybody!

Sorry, two things confused in thereā€¦

?. is the null conditional operator. It protects against null referenceā€¦ using it in this context was a bit of a mistake. It wonā€™t help anything since Iā€™m casting an object I know is not null.

Yes, () after a delegate is same as .Invoke(). Though, note this is not the same as DynamicInvoke(), which will use reflection to bind the parameters at runtime.

Also, your VoidVoidDelegate is basically just a duplicate of Action. Unless you are doing something custom here, I would suggest just using the types that are already part of the framework. Action for ā€œVoidVoidDelegateā€ Action for ā€œVoidTDelegateā€ Func for ā€œUVoidDelegateā€ and Func<T, U> for ā€œUTDelegateā€.

More: casting back to the base System.Delegate precludes the option to call Invoke ā€“ so you must call DynamicInvoke. This is much slower, will generate even more garbage than you are saving if you pass parameters, and requires reflection, which I donā€™t think is supported in Unity on all platforms. In your code, since you are only supporting VoidVoidDelegate right now, you should cast to this type (or better, just start using Action) rather than System.Delegate.

1 Like

This is cool, reminds me of bool trueOrFalse = myVar ? true : false;

Very nice!

Makes sense.
Awesome hints, thank you!