DelegatePool
Can implement a pool to cache instances of Delegate and lambda expressions.
com.katuusagi.delegatepool 
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
## 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
```
[](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
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
- Open [Window > Package Manager].
- click [+ > Add package from git url...].
- Type
https://github.com/Katsuya100/DelegatePool.git?path=packagesand 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.
No comments yet. Be the first!