はじめに
今回は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)に巨大データを直接渡すことが可能
導入
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#側のみのため、役割が異なります。
NativeMemory
.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