はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【Unity】Cysharp製NativeMemoryArrayを利用して、マネージドヒープを利用しない配列を確保する(要素数の制限なし)

はじめに

今回はNativeMemoryArrayというネイティブメモリを利用した配列を簡単に利用できるようにしたライブラリを紹介したいと思います。

NativeMemoryArray is a native-memory backed array for .NET and Unity. The array size of C# is limited to maximum index of 0x7FFFFFC7(2,147,483,591), Array.MaxLength. In terms of bytes[], it is about 2GB. This is very cheep in the modern world. We handle the 4K/8K videos, large data set of deep-learning, huge 3D scan data of point cloud, etc.

NativeMemoryArray provides the native-memory backed array, it supports infinity length, Span and Memory slices, IBufferWriter, ReadOnlySeqeunce and .NET 6's new Scatter/Gather I/O API.

NativeMemoryArrayは、.NETとUnityのためのネイティブメモリに裏打ちされた配列です。C#の配列サイズは、最大インデックス0x7FFFFC7(2,147,483,591)、Array.MaxLength.Byteに制限されます。bytes[]に換算すると約2GBになります。これは現代では非常にチープです。4K/8K動画、ディープラーニングの大容量データセット、点群の巨大3Dスキャンデータなどを扱います。

NativeMemoryArrayは、ネイティブメモリにバックアップされた配列を提供し、無限長、SpanとMemoryスライス、IBufferWriter、ReadOnlySeqeunce、.NET 6の新しい散布/収集入出力APIをサポートしています。

https://github.com/Cysharp/NativeMemoryArray#nativememoryarray

普通のC#の配列と比較して以下のメリットがあります。

  • ネイティブメモリから確保するためヒープを汚さない
  • 2GBの制限がなく、メモリの許す限り無限大の長さを確保できる
  • IBufferWriter 経由で、MessagePackSerializer, System.Text.Json.Utf8JsonWriter, System.IO.Pipelinesなどから直接読み込み可能
  • ReadOnlySequence 経由で、MessagePackSerializer, System.Text.Json.Utf8JsonReaderなどへ直接データを渡すことが可能
  • IReadOnlyList>, IReadOnlyList> 経由で RandomAccess(Scatter/Gather API)に巨大データを直接渡すことが可能

github.com
neue.cc

導入

PackageManagerから取ってきます。

https://github.com/Cysharp/NativeMemoryArray.git?path=src/NativeMemoryArray.Unity/Assets/Plugins/NativeMemoryArray

また依存先のライブラリは以下の通り。

  • System.Buffers.dll
  • System.Memory.dll
  • System.Runtime.CompilerServices.Unsafe.dll

PackageManagerから入れた場合は依存先のライブラリを自分で入れましょう。(nuget.org等から取ってきてPluginsフォルダの中に入れる)

もしくはReleaseページから取ってくれば依存先のライブラリも入っています。
github.com

使い方

NativeMemoryArray<T>のAPI一覧は以下の通り。

  • NativeMemoryArray(long length, bool skipZeroClear = false, bool addMemoryPressure = false)
  • long Length
  • ref T this[long index]
  • ref T GetPinnableReference()
  • Span<T> AsSpan()
  • Span<T> AsSpan(long start)
  • Span<T> AsSpan(long start, int length)
  • Memory<T> AsMemory()
  • Memory<T> AsMemory(long start)
  • Memory<T> AsMemory(long start, int length)
  • Stream AsStream()
  • Stream AsStream(long offset)
  • Stream AsStream(FileAccess fileAccess)
  • Stream AsStream(long offset, FileAccess fileAccess)
  • bool TryGetFullSpan(out Span<T> span)
  • IBufferWriter<T> CreateBufferWriter()
  • SpanSequence AsSpanSequence(int chunkSize = int.MaxValue)
  • MemorySequence AsMemorySequence(int chunkSize = int.MaxValue)
  • IReadOnlyList<Memory<T>> AsMemoryList(int chunkSize = int.MaxValue)
  • IReadOnlyList<ReadOnlyMemory<T>> AsReadOnlyMemoryList(int chunkSize = int.MaxValue)
  • ReadOnlySequence<T> AsReadOnlySequence(int chunkSize = int.MaxValue)
  • SpanSequence GetEnumerator()
  • void Dispose()

450行くらいなので、実装も貼っちゃいます。
github.com

// 配列自体はManaged Heapを利用しないが、NativeMemoryArray自体がclassなのでその分のGC.Allocは発生する
// Tはunmangedなら入れられる(参考:https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/builtin-types/unmanaged-types)
using NativeMemoryArray<byte> buffer = new NativeMemoryArray<byte>(100);
        
// ref T this[long index]
buffer[0] = 1;
        
// NativeMemoryArray.Length
Debug.Log(buffer.Length);       // 100
        
// Span
Span<byte> span = buffer.AsSpan();
span[1] = 2;

// Memory (Spanと違ってclassのフィールドにしたりasync/awaitで使える奴)
Memory<byte> memory = buffer.AsMemory();
memory.Span[2] = 3;

// 10個の要素単位で切り分ける
NativeMemoryArray<byte>.SpanSequence spanSequence = buffer.AsSpanSequence(10);
foreach (Span<byte> s in spanSequence)
{
    s[0] = 100;
}
Debug.Log(buffer[10]);        // 100
Debug.Log(buffer[20]);        // 100

// 10個ずつ切り分ける
foreach (Memory<byte> m in buffer.AsMemorySequence(10))
{
    m.Span[1] = 101;
}
Debug.Log(buffer[11]);        // 101
Debug.Log(buffer[21]);        // 101
        
// ReadOnly
foreach (ReadOnlyMemory<byte> m in buffer.AsReadOnlySequence(10)){}
        
// Stream
using Stream stream = buffer.AsStream();
var x = stream.ReadByte();
        
// IBufferWriter<T>
// NOTE : https://learn.microsoft.com/ja-jp/dotnet/standard/io/buffers
IBufferWriter<byte> writer = buffer.CreateBufferWriter();
Span<byte> writeSpan = writer.GetSpan(5);
ReadOnlySpan<char> helloSpan = "Hello".AsSpan();
int written = Encoding.ASCII.GetBytes(helloSpan, span);
writer.Advance(written);

NativeArrayとNativeMemoryArray

The difference between NativeArray and NativeArray in Unity is that NativeArray is a container for efficient interaction with the Unity Engine(C++) side. NativeMemoryArray has a different role because it is for C# side only.

UnityのNativeArrayとの違いは、NativeArrayはUnity Engine(C++)側と効率的にやり取りするためのコンテナです。NativeMemoryArrayはC#側のみのため、役割が異なります。

https://github.com/Cysharp/NativeMemoryArray#unity

NativeMemory

learn.microsoft.com

.NET 6からNativeMemoryというクラスが新たに追加されました。その名の通り、ネイティブメモリを扱いやすくするものです。今までもMarshal.AllocHGlobalといったメソッド経由でネイティブメモリを確保することは可能であったので、何が違うのか、というと、何も違いません。実際NativeMemoryArrayの .NET 6以前版はMarshalを使ってますし。そして .NET 6 では Marshal.AllocHGlobal は NativeMemory.Alloc を呼ぶので、完全に同一です。

https://neue.cc/2021/12/22.html

ただまだUnityでは対応していません。(はず)

適応対象
.NET 6, 7