This library "BoxingPool" provides extremely lightweight Boxing. Zero-allocation Boxing is achieved by pooling Boxing objects in advance and reusing them when needed.
| Environment | Version |
|---|---|
| Unity | 2021.3.28f1, 2022.3.20f1 |
| .Net | 4.x, Standard 2.1 |
| Process | Time |
|---|---|
| Boxing_Legacy | 31.004235 ms |
| Boxing_Pool | 15.1678 ms |
| Boxing_StructOnlyPool | 15.358 ms |
| Boxing_ConcurrentPool | 15.4086 ms |
| Boxing_ConcurrentStructOnlyPool | 15.39855 ms |
| Boxing_ThreadStaticPool | 15.19975 ms |
| Boxing_ThreadStaticStructOnlyPool | 15.3461 ms |
Using BoxingPool, the performance improvement is about 2x.
Also, allocation has been reduced to zero, and memory performance has been improved.
*Concurrent-type Pools will have allocations when returned.
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();
Big big = default(Big);
using (new Measure("Boxing_Legacy", log))
{
for (int i = 0; i < 5000; ++i)
{
big = new Big()
{
value = i,
};
object o = big;
Method(o);
}
}
using (new Measure("Boxing_Pool", log))
{
for (int i = 0; i < 5000; ++i)
{
big = new Big()
{
value = i,
};
object o = BoxingPool<Big>.Get(big);
Method(o);
BoxingPool<Big>.Return(o);
}
}| Process | Mono | IL2CPP |
|---|---|---|
| Boxing_Legacy | 52.86654 ms | 41.43652 ms |
| Boxing_Pool | 9.189175 ms | 2.425781 ms |
| Boxing_StructOnlyPool | 9.142063 ms | 2.452148 ms |
| Boxing_ConcurrentPool | 9.591019 ms | 3.321289 ms |
| Boxing_ConcurrentStructOnlyPool | 9.610249 ms | 3.245117 ms |
| Boxing_ThreadStaticPool | 9.15694 ms | 2.49707 ms |
| Boxing_ThreadStaticStructOnlyPool | 9.292259 ms | 2.520508 ms |
We saw a performance improvement of about 17x.
- Open [Window > Package Manager].
- click [+ > Add package from git url...].
- Type
https://github.com/Katsuya100/BoxingPool.git?path=packagesand click [Add].
The above method may not work well in environments where git is not installed.
Download the appropriate version of com.katuusagi.boxingpool.tgz from Releases, and then [Package Manager > + > Add package from tarball...] Use [Package Manager > + > Add package from tarball...] to install the package.
Download the appropriate version of Katuusagi.BoxingPool.unitypackage from Releases and Import it into your project from [Assets > Import Package > Custom Package].
BoxingPool usage with the following notation.
When using BoxingPool, please return objects as much as possible.
If not, the cache in the Pool will be reduced, which may lead to performance degradation.
object o = BoxingPool<int>.Get(100);
Debug.Log(o);
BoxingPool<int>.Return(o);If it is troublesome to return, the following notation is also valid.
using(BoxingPool<int>.Get(100, out object o))
{
Debug.Log(o);
}If a Pool of type Class is performed as follows, no cache is built and normal casting is performed.
var o = BoxingPool<GameObject>.Get(gameObject);Therefore, passing the Generic argument as follows works fine.
void Method<T>(T v)
{
// If the T-type is struct type, boxing costs are reduced.
// If the T type is a class type, it is cast.
var o = BoxingPool<T>.Get(v);
:
}If the type is definitely struct, StructOnlyBoxingPool can be used for better theoretical performance.
object o = StructOnlyBoxingPool<int>.Get(100);
Debug.Log(o);
StructBoxingPool<int>.Return(o);Boxing to base classes other than object can be realized with the following notation.
object o = BoxingPool<int, IComparable>.Get(100);If you want to use it in a multi-threaded environment Use ConcurrentBoxingPool or ConcurrentStructOnlyBoxingPool.
var o = ConcurrentBoxingPool<GameObject>.Get(gameObject);Unlike other BoxingPools, the Concurrent series has a unique Pool.
This allows it to be used in multi-threaded environments.
However, there are some performance issues compared to BoxingPool.
Specifically, allocation occurs at Return.
Plan to improve this in later updates.
Multithreading can be supported without performance loss by using ThreadStaticBoxingPool or ThreadStaticStructOnlyBoxingPool.
var o = ThreadStaticBoxingPool<GameObject>.Get(gameObject);Since a different pool is used for each Thread, memory consumption may be higher than in the Concurrent series.
Also, be careful not to return the object you get in a different thread.
The return will be completed normally, but it will be returned to a different pool from the one in which it was acquired.
You can create a cache in advance by calling the MakeCache function.
Creating a cache in advance allows you to determine the cache size and suppress allocation on the first run.
BoxingPool<int>.MakeCache(32);BoxingPool can be disabled to isolate the problem in the event of a defect.
To disable it, define the following symbols.
DISABLE_BOXING_POOL
After disabling, the API will still be valid, but normal Boxing will occur without the Pool.
By nature, storing a Boxed object does not allow you to rewrite the structure instance inside.
If you try to rewrite it normally, it will be reboxed and changed to another instance.
However, this library rewrites instances with the IL instruction to achieve reuse.
Also, Pool can be retrieved at high speed using Static Type Caching.
Although allocation of cache construction is performed at the first access, this allocation can be reduced to zero if the cache is created in advance.
The MethodImpl attribute is set to AggressiveInline, so you can also expect optimization by inline expansion at build time.
The above techniques provide overwhelming performance compared to conventional Boxing.