Skip to content

Repository files navigation

Hezium.Memory

NuGet Version

A contiguous, garbage-collected managed array for .NET with more than 2 billion elements.

BigArray<T>, BigMemory<T>, BigReadOnlyMemory<T>, BigSpan<T>, and BigReadOnlySpan<T> for .NET code that wants the Array/Memory/Span programming model with nint lengths and indexes.

The standard T[] and Span<T> APIs are excellent until the array you want to model is larger than the largest single managed array. Hezium.Memory keeps the surface area familiar: allocate an owner, take a span-like view, slice it, search it, copy it, sort it, and pass references around without inventing a second indexing style.

dotnet add package Hezium.Memory
using Hezium.Memory;

nint length = 10_000_000_000;
BigArray<byte> buffer = new(length);

buffer[0] = 1;
buffer[5_000_000_000] = 2;
buffer[length - 1] = 3;

BigSpan<byte> window = buffer.AsBigSpan(5_000_000_000, 1024);

window[0] = 42;
Console.WriteLine(buffer[5_000_000_000]); // 42

BigArray<T> stores its elements in one contiguous region of managed memory, even when its length exceeds Array.MaxLength. The storage is GC-managed, so reference-type elements are tracked and the allocation is collected when no longer used.

For explicit GC-style allocation, use GC.AllocateBigArray<T>() or GC.AllocateUninitializedBigArray<T>(). Both APIs accept the same pinned option as the built-in GC array allocation helpers.

BigArray<byte> zeroed = GC.AllocateBigArray<byte>(length);
BigArray<byte> scratch = GC.AllocateUninitializedBigArray<byte>(length);
BigArray<byte> pinned = GC.AllocateBigArray<byte>(length, pinned: true);

For more details about how this library was built, see the Introduction.

Why

Some workloads are naturally one-dimensional and very large: columnar data, precomputed lookup tables, native interop buffers, generated datasets, simulation state, and file-backed processing pipelines. The important part is not only "can I allocate a lot of elements", but "can I keep using the same mental model when I do?"

Hezium.Memory is built around that goal:

  • BigArray<T> is the owning storage type.
  • BigMemory<T> is the mutable storable view.
  • BigReadOnlyMemory<T> is the read-only storable view.
  • BigSpan<T> is the mutable stack-only view.
  • BigReadOnlySpan<T> is the read-only stack-only view.
  • Lengths, indexes, and offsets are nint.
  • APIs intentionally resemble Array, Memory<T>, ReadOnlyMemory<T>, Span<T>, ReadOnlySpan<T>, and MemoryMarshal.

BigArray

BigArray<T> is a one-dimensional, zero-based collection with a length that can exceed Array.MaxLength.

using Hezium.Memory;

BigArray<int> array = new(10);

array.Fill(-1);
array[0] = 10;
array[array.Length - 1] = 90;

nint middle = array.IndexOf(-1);
bool hasNinety = array.Contains(90);

Console.WriteLine($"{array.Length}, {middle}, {hasNinety}");

The maximum length depends on the element size:

nint byteCapacity = BigArray<byte>.MaxLength;
nint longCapacity = BigArray<long>.MaxLength;

Console.WriteLine(byteCapacity > Array.MaxLength);
Console.WriteLine(longCapacity > Array.MaxLength);

Unlike a jagged array, a BigArray<T> does not divide its elements among multiple backing arrays. It remains contiguous beyond Array.MaxLength and exposes the entire allocation through one nint-indexed space.

BigArray<byte> buffer = new((nint)Array.MaxLength + 1024);

nint last = buffer.Length - 1;
buffer[last] = 255;

Console.WriteLine(buffer[last]);

BigSpan

BigSpan<T> is a ref struct view over a contiguous region. It can be created from a BigArray<T>, a normal Span<T>, a reference, or a pointer.

using Hezium.Memory;

BigArray<int> owner = new(1024);
BigSpan<int> span = owner.AsBigSpan();

span.Fill(1);

BigSpan<int> block = span.Slice(128, 256);
block.Clear();

owner[128] = 123;
Console.WriteLine(block[0]); // 123

The type is deliberately span-shaped: indexing returns by reference, slicing is cheap, foreach works, and GetPinnableReference() is available for pinning scenarios.

Span<int> small = stackalloc int[] { 1, 2, 3, 4 };
BigSpan<int> big = small;

foreach (ref int item in big)
{
    item *= 2;
}

BigReadOnlySpan

BigReadOnlySpan<T> is the read-only counterpart. BigSpan<T> converts to it implicitly, and normal Span<T>/ReadOnlySpan<T> values can be used as small read-only big spans.

BigArray<int> data = new(5);
data.AsBigSpan().Fill(7);

BigReadOnlySpan<int> readOnly = data.AsBigSpan();

ref readonly int first = ref readOnly[0];
BigReadOnlySpan<int> tail = readOnly.Slice(1);

Console.WriteLine(first);
Console.WriteLine(tail.Length);

For text, take an explicit int-sized window before creating a string:

BigReadOnlySpan<char> text = "hello".ToCharArray();
string middle = text.Slice(1, 3).ToSpan(0, 3).ToString();

Console.WriteLine(middle); // ell

BigMemory

BigMemory<T> and BigReadOnlyMemory<T> mirror the storable Memory<T>/ReadOnlyMemory<T> shape with nint lengths. They can be sliced, copied, pinned, converted to arrays, and materialized as big spans when you need the hot by-ref path.

using Hezium.Memory;

BigArray<int> owner = new(1024);
BigMemory<int> memory = owner.AsBigMemory(128, 256);

memory.Span.Fill(7);

BigMemory<int> tail = memory.Slice(128);
BigReadOnlyMemory<int> readOnly = memory;

Console.WriteLine(tail.Length);
Console.WriteLine(readOnly.Span[0]);

Normal arrays and array segments convert to big memory without copying:

int[] small = [1, 2, 3, 4];

BigMemory<int> memory = small;
BigReadOnlyMemory<int> window = new ArraySegment<int>(small, 1, 2);

memory.Span[2] = 30;

Console.WriteLine(window.Span[1]); // 30
Console.WriteLine(small[2]); // 30

Span-Like Operations

The extension methods follow the Span<T> vocabulary, but return nint where an index can be large.

BigArray<int> numbers = new(8);
BigSpan<int> span = numbers.AsBigSpan();

for (nint i = 0; i < span.Length; i++)
{
    span[i] = (int)(i % 4);
}

nint firstTwo = span.IndexOf(2);
nint lastTwo = span.LastIndexOf(2);
bool startsWith = span.StartsWith(new[] { 0, 1 });

Console.WriteLine($"{firstTwo}, {lastTwo}, {startsWith}");

Copying works between big memory, big spans, normal spans, and BigArray<T>:

BigArray<int> source = new(4);
source.AsBigSpan().Fill(9);

BigArray<int> destination = new(8);
source.CopyTo(destination, destinationIndex: 2);

int[] small = new int[4];
source.AsBigSpan().CopyTo(small);

BigMemory<int> memory = source.AsBigMemory();
BigMemory<int> memoryDestination = destination.AsBigMemory(2, 4);
memory.CopyTo(memoryDestination);

Search, trim, split, compare, and sort operations are available where the underlying .NET span APIs support them:

BigSpan<int> values = new int[] { 0, 2, 1, 2, 0 };

BigSpan<int> trimmed = values.Trim(0);
nint marker = values.IndexOfAny(1, 9);

foreach (BigReadOnlySpan<int> segment in values.Split(2))
{
    Console.WriteLine(segment.Length);
}

values.Sort();

SearchValues<T> is supported for repeated searches over compatible element types:

using System.Buffers;
using Hezium.Memory;

BigReadOnlySpan<byte> bytes = new byte[] { 1, 2, 3, 2, 1 };
SearchValues<byte> separators = SearchValues.Create((ReadOnlySpan<byte>)[2, 9]);

nint firstSeparator = bytes.IndexOfAny(separators);

foreach (BigReadOnlySpan<byte> segment in bytes.SplitAny(separators))
{
    Console.WriteLine(segment.Length);
}

The maximum number of elements processed by the underlying span operations at once can be configured at application startup:

<ItemGroup>
  <RuntimeHostConfigurationOption Include="Hezium.Memory.MaxProcessingChunkLength" Value="1048576" />
</ItemGroup>

Hezium.Memory.MaxProcessingChunkLength defaults to Array.MaxLength. It lets applications tune the processing granularity of span-based algorithms for their workload; smaller chunks also introduce more per-chunk overhead. The value is process-wide, is read once, and does not change the BigArray<T> storage layout.

The configured value is clamped to the range from 65536 through Array.MaxLength.

MemoryMarshal Helpers

MemoryMarshal extension members make it possible to create and inspect big spans from raw references.

using System.Runtime.InteropServices;
using Hezium.Memory;

int value = 42;

BigSpan<int> span = MemoryMarshal.CreateBigSpan(ref value, length: 1);
ref int reference = ref MemoryMarshal.GetReference(span);

reference++;

BigReadOnlySpan<int> readOnly = span;
ref readonly int readOnlyReference = ref MemoryMarshal.GetReference(readOnly);

Console.WriteLine(readOnlyReference); // 43

API Map

Type Purpose
BigArray<T> Owning storage with nint Length, MaxLength, indexer, enumeration, AsBigSpan, AsBigMemory, and AsSpan for int-sized windows.
BigMemory<T> Mutable storable view with nint Length, slicing, Span, copy/try-copy, pinning, ToArray, and ToBigArray.
BigReadOnlyMemory<T> Read-only storable view with nint Length, slicing, Span, copy/try-copy, pinning, ToArray, and ToBigArray.
BigSpan<T> Mutable ref struct view with nint Length, slicing, by-ref indexing, pinning, enumeration, copy/search/trim/split/sort helpers.
BigReadOnlySpan<T> Read-only ref struct view with slicing, by-readonly-ref indexing, pinning, enumeration, copy/search/trim/split helpers.
MemoryMarshal extensions CreateBigSpan, GetReference(BigSpan<T>), and GetReference(BigReadOnlySpan<T>).
GC extensions AllocateBigArray<T> and AllocateUninitializedBigArray<T> with optional pinned storage.

Requirements

  • .NET 10 or later

Notes

  • BigMemory<T> and BigReadOnlyMemory<T> are regular structs that can be stored; their Span properties produce stack-only big span views.
  • BigSpan<T> and BigReadOnlySpan<T> are ref struct types, so they follow the same stack-only lifetime rules as Span<T>.
  • ToArray() requires the span or array to fit into a single T[]; use ToBigArray() when the result may exceed Array.MaxLength.
  • BigArray<T>.MaxLength is element-size dependent.
  • Value types larger than 65535 bytes are rejected.

Benchmarks

These benchmarks compare BigArray<T> with T[][] (a jagged array) at equivalent logical lengths.

Environment:

  • CPU: AMD EPYC 9V74 with 48 cores
  • OS: Ubuntu 24.04.4 LTS (GNU/Linux 6.17.0-1018-azure x86_64)
  • Memory: 192 GB
  • .NET: 10.0.109

AllocationBenchmarks

Method Job Server Length Mean Error StdDev Gen0 Gen1 Gen2 Allocated
JaggedArray Job-MSAXQN False 1048576 107.77 μs 0.579 μs 0.513 μs 333.2520 332.8857 332.8857 4 MB
BigArray Job-MSAXQN False 1048576 109.14 μs 1.923 μs 1.705 μs 330.5664 330.2002 330.2002 4 MB
JaggedArray Job-WGBUQW True 1048576 130.19 μs 2.523 μs 3.454 μs 3.1738 3.1738 3.1738 4 MB
BigArray Job-WGBUQW True 1048576 127.78 μs 2.468 μs 2.937 μs 3.4180 3.4180 3.4180 4 MB
JaggedArray Job-MSAXQN False 4294967296 320,082.17 μs 60,406.206 μs 90,413.172 μs 500.0000 500.0000 500.0000 16384 MB
BigArray Job-MSAXQN False 4294967296 251,707.29 μs 112.220 μs 99.480 μs 500.0000 500.0000 500.0000 16384.06 MB
JaggedArray Job-WGBUQW True 4294967296 179.18 μs 3.907 μs 5.215 μs - - - 16384 MB
BigArray Job-WGBUQW True 4294967296 40.13 μs 4.645 μs 6.953 μs - - - 16384.06 MB

IndexedAccessBenchmarks

Method Job Server Length Mean Error StdDev Allocated
JaggedRandomLoad Job-MSAXQN False 1048576 15.753 μs 0.0074 μs 0.0061 μs -
BigArrayRandomLoad Job-MSAXQN False 1048576 8.145 μs 0.0628 μs 0.0588 μs -
JaggedRandomLoad Job-WGBUQW True 1048576 16.092 μs 0.0524 μs 0.0490 μs -
BigArrayRandomLoad Job-WGBUQW True 1048576 7.952 μs 0.1058 μs 0.0990 μs -
JaggedRandomLoad Job-MSAXQN False 4294967296 26.112 μs 0.0270 μs 0.0239 μs -
BigArrayRandomLoad Job-MSAXQN False 4294967296 16.648 μs 0.0171 μs 0.0160 μs -
JaggedRandomLoad Job-WGBUQW True 4294967296 26.072 μs 0.1456 μs 0.1290 μs -
BigArrayRandomLoad Job-WGBUQW True 4294967296 16.198 μs 0.0296 μs 0.0277 μs -
JaggedRandomStore Job-MSAXQN False 1048576 16.459 μs 0.0032 μs 0.0029 μs -
BigArrayRandomStore Job-MSAXQN False 1048576 17.277 μs 0.0098 μs 0.0082 μs -
JaggedRandomStore Job-WGBUQW True 1048576 51.193 μs 0.0789 μs 0.0738 μs -
BigArrayRandomStore Job-WGBUQW True 1048576 17.200 μs 0.0093 μs 0.0087 μs -
JaggedRandomStore Job-MSAXQN False 4294967296 25.410 μs 0.0359 μs 0.0336 μs -
BigArrayRandomStore Job-MSAXQN False 4294967296 19.036 μs 0.0148 μs 0.0131 μs -
JaggedRandomStore Job-WGBUQW True 4294967296 26.077 μs 0.0445 μs 0.0416 μs -
BigArrayRandomStore Job-WGBUQW True 4294967296 18.947 μs 0.0142 μs 0.0126 μs -

SearchValuesBenchmarks

Method Job Server Length Mean Error StdDev Allocated
JaggedIndexOfAny Job-MSAXQN False 1048576 35,037.717 ns 8.3021 ns 7.7658 ns -
BigArrayIndexOfAny Job-MSAXQN False 1048576 34,034.869 ns 114.1575 ns 101.1977 ns -
JaggedIndexOfAny Job-WGBUQW True 1048576 33,834.867 ns 636.5404 ns 653.6801 ns -
BigArrayIndexOfAny Job-WGBUQW True 1048576 34,216.534 ns 5.1605 ns 4.8272 ns -
JaggedIndexOfAny Job-MSAXQN False 4294967296 149,024,225.036 ns 72,304.7208 ns 64,096.2517 ns -
BigArrayIndexOfAny Job-MSAXQN False 4294967296 149,208,981.917 ns 148,120.4669 ns 138,551.9767 ns -
JaggedIndexOfAny Job-WGBUQW True 4294967296 149,841,356.783 ns 164,965.1421 ns 154,308.4964 ns -
BigArrayIndexOfAny Job-WGBUQW True 4294967296 149,210,761.017 ns 178,532.2659 ns 166,999.1924 ns -
JaggedLastIndexOfAny Job-MSAXQN False 1048576 3.743 ns 0.0041 ns 0.0039 ns -
BigArrayLastIndexOfAny Job-MSAXQN False 1048576 3.622 ns 0.0052 ns 0.0049 ns -
JaggedLastIndexOfAny Job-WGBUQW True 1048576 3.710 ns 0.0062 ns 0.0058 ns -
BigArrayLastIndexOfAny Job-WGBUQW True 1048576 3.620 ns 0.0049 ns 0.0046 ns -
JaggedLastIndexOfAny Job-MSAXQN False 4294967296 3.717 ns 0.0116 ns 0.0108 ns -
BigArrayLastIndexOfAny Job-MSAXQN False 4294967296 5.105 ns 0.0097 ns 0.0086 ns -
JaggedLastIndexOfAny Job-WGBUQW True 4294967296 3.746 ns 0.0059 ns 0.0053 ns -
BigArrayLastIndexOfAny Job-WGBUQW True 4294967296 5.126 ns 0.0131 ns 0.0123 ns -

SpanAlgorithmBenchmarks

Method Job Server Length Mean Error StdDev Median Allocated
JaggedBinarySearch Job-MSAXQN False 1048576 49.96 ns 0.011 ns 0.010 ns 49.96 ns -
BigArrayBinarySearch Job-MSAXQN False 1048576 18.71 ns 0.005 ns 0.004 ns 18.71 ns -
JaggedBinarySearch Job-WGBUQW True 1048576 52.47 ns 0.027 ns 0.025 ns 52.47 ns -
BigArrayBinarySearch Job-WGBUQW True 1048576 18.70 ns 0.012 ns 0.011 ns 18.70 ns -
JaggedBinarySearch Job-MSAXQN False 4294967296 96.54 ns 0.144 ns 0.127 ns 96.55 ns -
BigArrayBinarySearch Job-MSAXQN False 4294967296 85.78 ns 0.012 ns 0.011 ns 85.79 ns -
JaggedBinarySearch Job-WGBUQW True 4294967296 96.07 ns 0.578 ns 0.541 ns 96.05 ns -
BigArrayBinarySearch Job-WGBUQW True 4294967296 85.48 ns 0.510 ns 0.477 ns 85.58 ns -
JaggedCopyTo Job-MSAXQN False 1048576 98,710.30 ns 56.256 ns 52.622 ns 98,715.92 ns -
BigArrayCopyTo Job-MSAXQN False 1048576 98,145.87 ns 20.593 ns 17.196 ns 98,141.40 ns -
JaggedCopyTo Job-WGBUQW True 1048576 98,778.12 ns 29.881 ns 26.489 ns 98,776.77 ns -
BigArrayCopyTo Job-WGBUQW True 1048576 98,077.99 ns 128.374 ns 113.800 ns 98,054.42 ns -
JaggedCopyTo Job-MSAXQN False 4294967296 873,535,712.00 ns 1,497,004.297 ns 1,400,298.749 ns 873,265,018.00 ns -
BigArrayCopyTo Job-MSAXQN False 4294967296 880,486,546.20 ns 3,345,173.644 ns 3,129,077.503 ns 879,058,670.00 ns -
JaggedCopyTo Job-WGBUQW True 4294967296 884,969,636.00 ns 2,330,855.056 ns 2,066,242.296 ns 885,234,089.50 ns -
BigArrayCopyTo Job-WGBUQW True 4294967296 879,429,365.60 ns 4,077,759.552 ns 3,814,338.817 ns 880,100,492.00 ns -
JaggedFill Job-MSAXQN False 1048576 58,367.10 ns 180.325 ns 168.676 ns 58,243.54 ns -
BigArrayFill Job-MSAXQN False 1048576 45,894.35 ns 18.216 ns 17.039 ns 45,899.12 ns -
JaggedFill Job-WGBUQW True 1048576 58,374.97 ns 288.436 ns 269.803 ns 58,219.81 ns -
BigArrayFill Job-WGBUQW True 1048576 45,931.93 ns 61.103 ns 57.156 ns 45,923.62 ns -
JaggedFill Job-MSAXQN False 4294967296 640,167,967.50 ns 272,398.007 ns 212,670.447 ns 640,128,911.50 ns -
BigArrayFill Job-MSAXQN False 4294967296 639,780,247.00 ns 1,802,039.933 ns 1,406,914.247 ns 640,167,750.50 ns -
JaggedFill Job-WGBUQW True 4294967296 641,006,562.00 ns 3,491,797.738 ns 2,915,809.256 ns 640,610,855.00 ns -
BigArrayFill Job-WGBUQW True 4294967296 637,157,023.75 ns 1,381,366.359 ns 1,078,479.991 ns 636,751,010.50 ns -
JaggedSequenceEqual Job-MSAXQN False 1048576 101,913.46 ns 158.225 ns 148.004 ns 101,941.02 ns -
BigArraySequenceEqual Job-MSAXQN False 1048576 95,134.55 ns 118.142 ns 110.510 ns 95,132.74 ns -
JaggedSequenceEqual Job-WGBUQW True 1048576 102,195.72 ns 234.248 ns 219.116 ns 102,142.76 ns -
BigArraySequenceEqual Job-WGBUQW True 1048576 95,224.84 ns 115.790 ns 108.310 ns 95,233.91 ns -
JaggedSequenceEqual Job-MSAXQN False 4294967296 1,020,484,182.52 ns 63,587,008.014 ns 93,205,019.553 ns 968,937,213.00 ns -
BigArraySequenceEqual Job-MSAXQN False 4294967296 1,029,241,139.70 ns 72,403,060.080 ns 108,369,500.092 ns 962,084,258.50 ns -
JaggedSequenceEqual Job-WGBUQW True 4294967296 979,557,987.45 ns 81,645,292.812 ns 119,674,621.446 ns 914,186,446.00 ns -
BigArraySequenceEqual Job-WGBUQW True 4294967296 969,798,814.43 ns 75,013,740.364 ns 112,277,043.737 ns 904,311,547.50 ns -

License

MIT License.

About

Contiguous, garbage-collected managed BigArray, BigMemory, and BigSpan types for .NET languages (C#, F#, etc.), supporting huge arrays with more than 2 billion elements and up to 128 TiB using GC-managed contiguous memory and nint indexing.

Topics

Resources

Code of conduct

Stars

47 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages