Unclaimed Package Is this your package? Claim it to unlock full analytics and manage your listing.
Claim This Package

Install via UPM

Add to Unity Package Manager using this URL

https://www.pkglnk.dev/delegatepool.git?path=packages

README Markdown

Copy this to your project's README.md

Style
Preview
pkglnk installs badge
## Installation

Add **DelegatePool** to your Unity project via Package Manager:

1. Open **Window > Package Manager**
2. Click **+** > **Add package from git URL**
3. Enter:
```
https://www.pkglnk.dev/delegatepool.git?path=packages
```

[![pkglnk](https://www.pkglnk.dev/badge/delegatepool.svg?style=pkglnk)](https://www.pkglnk.dev/pkg/delegatepool)

README

DelegatePool

日本語

Summary

This library, "DelegatePool," suppresses allocation when generating delegates.
Delegate objects are pooled in advance and reused when necessary to realize zero-allocation delegate generation.
Cache instances generated by lambda expressions can also be pooled.

System Requirements

Environment Version
Unity 6000.0.51f1
.Net 4.x, Standard 2.1

Performance

Measurement code on the editor

Test Code.

Result

Process Time
Instance_Legacy 0.2419 ms
Instance_Pool 1.81385 ms
Instance_ConcurrentPool 1.9628 ms
Instance_ThreadStaticPool 1.7624 ms
Lambda_Legacy 0.4348 ms
Lambda_Pool 2.8968 ms
Lambda_ConcurrentPool 3.52965 ms
Lambda_ThreadStaticPool 2.8054 ms

Pool is more expensive in the editor environment, but the IL2CPP environment, described below, yields different results.

Measurement code on the runtime

private readonly ref struct Measure
{
    private readonly string _label;
    private readonly StringBuilder _builder;
    private readonly float _time;

    public Measure(string label, StringBuilder builder)
    {
        _label = label;
        _builder = builder;
        _time = (Time.realtimeSinceStartup * 1000);
    }

    public void Dispose()
    {
        _builder.AppendLine($"{_label}: {(Time.realtimeSinceStartup * 1000) - _time} ms");
    }
}

 :

var log = new StringBuilder();
var t = new TestFunctions.Test();
using (new Measure("Instance_Legacy", log))
{
    for (int i = 0; i < 5000; ++i)
    {
        Instance_Legacy(t);
    }
}

using (new Measure("Instance_Pool", log))
{
    for (int i = 0; i < 5000; ++i)
    {
        Instance_Pool(t);
    }
}

 :

public void Instance_Legacy(TestFunctions.Test t)
{
    Func<int> f = t.Return1;
    f();
}

public void Instance_Pool(TestFunctions.Test t)
{
    using (DelegatePool<Func<int>>.Get(t.Return1, out var f))
    {
        f();
    }
}

Result

Process Mono IL2CPP
Instance_Legacy 1.375793 ms 1.275879 ms
Instance_Pool 1.507324 ms 0.2495117 ms
Instance_ConcurrentPool 2.047913 ms 1.229492 ms
Instance_ThreadStaticPool 1.435272 ms 0.300293 ms
Lambda_Legacy 1.321472 ms 1.114258 ms
Lambda_Pool 2.068634 ms 0.4941406 ms
Lambda_ConcurrentPool 3.389587 ms 2.715332 ms
Lambda_ThreadStaticPool 1.974792 ms 0.6586914 ms

The performance improvement is about 5x in an IL2CPP environment.
Memory performance is also eco-friendly because allocations can be suppressed.

How to install

Install dependenies

Install the following packages.

Installing DelegatePool

  1. Open [Window > Package Manager].
  2. click [+ > Add package from git url...].
  3. Type https://github.com/Katsuya100/DelegatePool.git?path=packages and click [Add].

If it doesn't work

The above method may not work well in environments where git is not installed.
Download the appropriate version of com.katuusagi.delegatepool.tgz from Releases, and then [Package Manager > + > Add package from tarball...] Use [Package Manager > + > Add package from tarball...] to install the package.

If it still doesn't work

Download the appropriate version of Katuusagi.DelegatePool.unitypackage from Releases and Import it into your project from [Assets > Import Package > Custom Package].

How to Use

Normal usage

DelegatePool can be used with the following notation.
If you do not use the using statement(or await using statement), performance may be degraded due to release leaks.

public static void Hoge()
{
}

:

using(DelegatePool<Action>.Get(Hoge, out var a))
{
    a();
}

To the veteran it looks like Hoge is instantiated, but in reality it is not.
This implementation translate as follows

Action a;
DelegatePool<Action>.GetHandler classOnly = DelegatePool<Action>.GetClassOnly<DelegatePoolTest>(null, (nint)(delegate*<void>)(&Hoge), null, out a);
try
{
    a();
}
finally
{
    classOnly.Dispose();
}

It is also possible to keep the Handler as a member by casting it to the ReadOnlyHandler type.

private DelegatePool<Action>.ReadOnlyHandler _handle;

:

private void OnDestroy()
{
    _handle.Dispose();
}

:

_handle = DelegatePool<Action>.Get(Hoge, out var o);

Pool instances of lambda expressions

You can Pool instances of lambda expressions with the following notation.

int v = 1;
using(DelegatePool<Action>.Get(() => Debug.Log(v), out var a))
{
    a();
}
``
This implementation translates as follows
```.cs
IReferenceHandler h = default(IReferenceHandler);
try
{
    CountPool<<>c__DisplayClass13_0>.Get(ref h, out var result);
    result.v = 1;
    Action a;
    DelegatePool<Action>.GetHandler classOnly = DelegatePool<Action>.GetClassOnly(result, (nint)__ldftn(<>c__DisplayClass13_0.<A>b__0), h, out a);
    try
    {
        a();
    }
    finally
    {
        classOnly.Dispose();
    }
}
finally
{
    CountPool<<>c__DisplayClass13_0>.Return(h);
}

The lambda expression is Pooled by CountPool.
CountPool is an ObjectPool that manages returns using reference counting.
Since DelegatePool references it, it can manage the return of lambda expressions.

If you want to support multi-threading

Use ConcurrentDelegatePool if you want to use it in a multi-threaded environment.

using(ConcurrentDelegatePool<Action>.Get(Hoge, out var a))
{
    a();
}

Unlike other DelegatePools, the Concurrent series has a unique Pool.
This allows them to be used in multi-threaded environments.
However, it has performance issues compared to DelegatePool.
Specifically, allocation occurs on Return.
We plan to improve this in a later update.

ThreadStatic pools

Using ThreadStaticDelegatePool allows for multi-threading without performance loss.

using(ThreadStaticDelegatePool<Action>.Get(Hoge, out var a))
{
    a();
}

Since different pools are used for each Thread, memory consumption may be higher than in the Concurrent series. Also, be careful not to Disose in different Threads.
The return will be completed normally, but it will be returned to a different pool than the pool from which it was obtained.

Reasons for high performance

Delegate and lambda expressions are instantiated invisibly to the programmer.
For this reason, it has been impossible to Pool them in the past. DelegatePool uses the ILPostProcessor to Pool invisible instances.

Since the MethodImpl attribute is set to AggressiveInline, optimization can be expected from inline expansion at build time.
With the above techniques, we have succeeded in instantiating Delegate with zero allocation.

Comments

No comments yet. Be the first!