Unity Collections Package
This package provides unmanaged data structures that can be used in jobs and Burst-compiled code.
The collections provided by this package fall into three categories:
- The collection types in
Unity.Collections
whose names start withNative-
have safety checks for ensuring that they're properly disposed and are used in a thread-safe manner. - The collection types in
Unity.Collections.LowLevel.Unsafe
whose names start withUnsafe-
do not have these safety checks. - The remaining collection types are not allocated and contain no pointers, so effectively their disposal and thread safety are never a concern. These types hold only small amounts of data.
The Native-
types perform safety checks to ensure that indexes passed to their methods are in bounds, but the other types in most cases do not.
Several Native-
types have Unsafe-
equivalents, e.g. NativeList
and UnsafeList
, NativeHashMap
and UnsafeHashMap
, among others.
While you should generally prefer using the Native-
collections over their Unsafe-
equivalents, Native-
collections cannot contain other Native-
collections (owing to the implementation of their safety checks). So if, say, you want a list of lists, you can have a NativeList<UnsafeList<T>>
or an UnsafeList<UnsafeList<T>>
, but you cannot have a NativeList<NativeList<T>>
.
When safety checks are disabled, there is generally no significant performance difference between a Native-
type and its Unsafe-
equivalent. In fact, most Native-
collections are implemented simply as wrappers of their Unsafe-
counterparts. For example, NativeList
is comprised of an UnsafeList
plus a few handles used by the safety checks.
The collection types
Array-like types
A few key array-like types are provided by the core module, including Unity.Collections.NativeArray<T>
and Unity.Collections.NativeSlice<T>
. This package itself provides:
Data structure | Description |
---|---|
NativeList<T> | A resizable list. Has thread- and disposal-safety checks. |
UnsafeList<T> | A resizable list. |
UnsafePtrList<T> | A resizable list of pointers. |
NativeStream | A set of append-only, untyped buffers. Has thread- and disposal-safety checks. |
UnsafeStream | A set of append-only, untyped buffers. |
UnsafeAppendBuffer | An append-only untyped buffer. |
NativeQueue<T> | A resizable queue. Has thread- and disposal-safety checks. |
UnsafeRingQueue<T> | A fixed-size circular buffer. |
FixedList32Bytes<T> | A 32-byte list, including 2 bytes of overhead, so 30 bytes are available for storage. Max capacity depends upon T. |
FixedList32Bytes<T>
has variants of larger sizes: FixedList64Bytes<T>
, FixedList128Bytes<T>
, FixedList512Bytes<T>
, FixedList4096Bytes<T>
.
There are no multi-dimensional array types, but you can simply pack all the data into a single-dimension. For example, for an int[4][5]
array, use an int[20]
array instead (because 4 * 5
is 20
).
When using the Entities package, a DynamicBuffer component is often the best choice for an array- or list-like collection.
See also NativeArrayExtensions, ListExtensions, NativeSortExtension.
Map and set types
Data structure | Description |
---|---|
NativeHashMap<TKey, TValue> | An unordered associative array of key-value pairs. Has thread- and disposal-safety checks. |
UnsafeHashMap<TKey, TValue> | An unordered associative array of key-value pairs. |
NativeHashSet<T> | A set of unique values. Has thread- and disposal-safety checks. |
UnsafeHashSet<T> | A set of unique values. |
NativeMultiHashMap<TKey, TValue> | An unordered associative array of key-value pairs. The keys do not have to be unique, i.e. two pairs can have equal keys. Has thread- and disposal-safety checks. |
UnsafeMultiHashMap<TKey, TValue> | An unordered associative array of key-value pairs. The keys do not have to be unique, i.e. two pairs can have equal keys. |
See also HashSetExtensions, Extensions, and Extensions
Bit arrays and bit fields
Data structure | Description |
---|---|
BitField32 | A fixed-size array of 32 bits. |
BitField64 | A fixed-size array of 64 bits. |
NativeBitArray | An arbitrary-sized array of bits. Has thread- and disposal-safety checks. |
UnsafeBitArray | An arbitrary-sized array of bits. |
String types
Data structure | Description |
---|---|
NativeText | A UTF-8 encoded string. Mutable and resizable. Has thread- and disposal-safety checks. |
FixedString32Bytes | A 32-byte UTF-8 encoded string, including 3 bytes of overhead, so 29 bytes available for storage. |
FixedString32Bytes
has variants of larger sizes: FixedString64Bytes
, FixedString128Bytes
, FixedString512Bytes
, FixedString4096Bytes
.
See also FixedStringMethods
Other types
Data structure | Description |
---|---|
NativeReference<T> | A reference to a single value. Functionally equivalent to an array of length 1. Has thread- and disposal-safety checks. |
UnsafeAtomicCounter32 | A 32-bit atomic counter. |
UnsafeAtomicCounter64 | A 64-bit atomic counter. |
Job safety checks
The purpose of the job safety checks is to detect job conflicts. Two jobs conflict if:
- Both jobs access the same data.
- One job or both jobs have write access to the data.
In other words, there's no conflict if both jobs just have read only access to the data.
For example, you generally wouldn't want one job to read an array while meanwhile another job is writing the same array, so the safety checks consider that possibility to be a conflict. To resolve such conflicts, you must make one job a dependency of the other to ensure their execution does not overlap. Whichever of the two jobs you want to run first should be the dependency of the other.
When the safety checks are enabled, each Native-
collection has an AtomicSafetyHandle
for performing thread-safety checks. Scheduling a job locks the AtomicSafetyHandle
's of all Native-
collections in the job. Completing a job releases the AtomicSafetyHandle
's of all Native-
collections in the job.
While a Native-
collection's AtomicSafetyHandle
is locked:
- Jobs which use the collection can only be scheduled if they depend upon all the already scheduled job(s) which also use it.
- Accessing the collection from the main thread will throw an exception.
Read only access in jobs
As a special case, there's no conflict between two jobs if they both strictly just read the same data, .e.g. there's no conflict if one job reads from an array while meanwhile another also job reads from the same array.
The ReadOnlyAttribute marks a Native-
collection in a job struct as being read only:
public struct MyJob : IJob
{
// This array can only be read in the job.
[ReadOnly] public NativeArray<int> nums;
public void Execute()
{
// If safety checks are enabled, an exception is thrown here
// because the array is read only.
nums[0] = 100;
}
}
Marking collections as read only has two benefits:
- The main thread can still read a collection if all scheduled jobs that use the collection have just read only access.
- The safety checks will not object if you schedule multiple jobs with read only access to the same collection, even without any dependencies between them. Therefore these jobs can run concurrently with each other.
Enumerators
Most of the collections have a GetEnumerator
method, which returns an implementation of IEnumerator<T>
. The enumerator's MoveNext
method advances its Current
property to the next element.
NativeList<int> nums = new NativeList<int>(10, Allocator.Temp);
// Calculate the sum of all elements in the list.
int sum = 0;
NativeArray<int>.Enumerator enumerator = nums.GetEnumerator();
// The first MoveNext call advances the enumerator to the first element.
// MoveNext returns false when the enumerator has advanced past the last element.
while (enumerator.MoveNext())
{
sum += enumerator.Current;
}
// The enumerator is no longer valid to use after the array is disposed.
nums.Dispose();
Parallel readers and writers
Several of the collection types have nested types for reading and writing from parallel jobs. For example, to write safely to a NativeList<T>
from a parallel job, you need a NativeList<T>.ParallelWriter
:
NativeList<int> nums = new NativeList<int>(1000, Allocator.TempJob);
// The parallel writer shares the original list's AtomicSafetyHandle.
var job = new MyParallelJob {NumsWriter = nums.AsParallelWriter()};
public struct MyParallelJob : IJobParallelFor
{
public NativeList<int>.ParallelWriter NumsWriter;
public void Execute(int i)
{
// A NativeList<T>.ParallelWriter can append values
// but not grow the capacity of the list.
NumsWriter.AddNoResize(i);
}
}
Note that these parallel readers and writers do not usually support the full functionality of the collection. For example, a NativeList
cannot grow its capacity in a parallel job (because there is no way to safely allow this without incurring significantly more synchronization overhead).
Deterministic reading and writing
Although a ParallelWriter
ensures the safety of concurrent writes, the order of the concurrent writes is inherently indeterminstic because it depends upon the happenstance of thread scheduling (which is controlled by the operating system and other factors outside of your program's control).
Likewise, although a ParallelReader
ensures the safety of concurrent reads, the order of the concurrent reads is inherently indeterminstic, so it can't be known which threads will read which values.
One solution is to use either NativeStream or UnsafeStream, which splits reads and writes into a separate buffer for each thread and thereby avoids indeterminism.
Alternatively, you can effectively get a deterministic order of parallel reads if you deterministically divide the reads into separate ranges and process each range in its own thread.
You also can effectively get a deterministic order of writes if you deterministically sort the data after it's written:
internal partial class MySystemCollections : SystemBase
{
protected override void OnUpdate()
{
// This artificial example copies all Foo component values to a
// list in parallel, then sorts the list based on entityInQueryIndex
// to make the order in the list deterministic.
// For simplicity, we'll assume we know that there are no
// more than 100 entities with a Foo component.
NativeList<SortableFoo> list = new NativeList<SortableFoo>(100, Allocator.TempJob);
NativeList<SortableFoo>.ParallelWriter writer = list.AsParallelWriter();
Entities.ForEach((int entityInQueryIndex, in Foo foo) =>
{
writer.AddNoResize(
new SortableFoo {SortKey = entityInQueryIndex, Foo = foo}
);
}).ScheduleParallel();
Dependency.Complete(); // Completes the job.
// Because the sort criteria does not depend upon scheduling happenstance,
// the resulting order is deterministic after the sort.
list.Sort(new SortableFooComparer());
// ... Use the sorted list.
}
}
internal struct SortableFoo
{
public int SortKey;
public Foo Foo;
}
internal struct SortableFooComparer : IComparer<SortableFoo>
{
public int Compare(SortableFoo x, SortableFoo y)
{
if (x.SortKey == y.SortKey)
return 0;
return (x.SortKey == y.SortKey) ? -1 : 1;
}
}