InlineCollections provides high-performance, zero-allocation collection primitives for .NET with fixed capacities of 8, 16, or 32 elements. The collections are implemented as ref struct types optimized for ultra-low latency scenarios where heap allocations must be eliminated.
InlineCollections provides three collection types (InlineList<T>, InlineStack<T>, InlineQueue<T>) in three fixed sizes (8, 16, and 32 elements), with stack-allocated storage via the InlineArray language feature (C# 12+), enabling:
- โจ Zero heap allocations for the fast path
- ๐๏ธ Minimal GC pressure
- ๐ฏ Predictable memory layout for cache optimization
- ๐ก๏ธ High-throughput, low-latency execution
Important
Positioning Statement: This library is not a general-purpose replacement for the standard BCL collections types. Standard collections are designed for flexibility and large datasets. InlineCollections are "surgical tools" designed for High-Performance hot-paths where the developer has a guaranteed bound on the number of elements (โค 32) and must eliminate heap allocations to reduce GC pressure and latency.
Choose your collection size based on typical working set:
- InlineList8/Stack8/Queue8: Minimal overhead, ultra-low latency (โค 8 elements)
- InlineList16/Stack16/Queue16: Balanced capacity and size (โค 16 elements)
- InlineList32/Stack32/Queue32: Maximum capacity within stack budget (โค 32 elements)
Add the package to your project via .NET CLI:
dotnet add package InlineCollections --version 0.2.0InlineCollections are ref struct types designed for stack allocation. They ensure Zero Heap Allocation for up to 32 elements.
using InlineCollections;
// Initialize on stack (Zero Allocation)
var list = new InlineList32<int>();
list.Add(10);
list.Add(20);
// High-performance iteration (Modify in-place via Span)
foreach (ref int item in list.AsSpan())
{
item += 1;
}
// โ ๏ธ When using large value types as elements, be aware that every
// pass-by-value of `list` copies the entire buffer (capacity * sizeof(T)).
// Passing by `ref` or `in` can avoid excessive copying on the stack.Standard .NET collections (List<T>, Stack<T>, Queue<T>) allocate on the heap, requiring GC overhead and cache misses for small working sets. In high-performance scenarios (real-time systems, game engines, network processors, serialization hotspots), this overhead is unacceptable. InlineCollections eliminates allocations by storing 32 elements inline within the struct itself.
Performance highlights
- โก Zero Allocations โ for up to 32 elements these collections avoid heap allocations.
- ๐๏ธ Reduced GC Pressure โ fewer short-lived allocations means fewer GC cycles and pauses.
- โ๏ธ Predictable Latency โ bounded-capacity operations reduce variance in execution time.
- โก Hot-path code that creates many short-lived small collections (โค 32 elements)
- โฑ๏ธ Real-time systems requiring predictable latency
- ๐ฎ Game engine frame-local processing (per-frame temporary buffers)
- โก Network packet processing and protocol parsing
- ๐ Serialization/deserialization buffers where allocations matter
- ๐ง Stack-like or frame-local data with bounded depth (8-32 elements)
- Collections that routinely exceed 32 elements
- Scenarios requiring thread-safety or concurrent access
- Reference-type or nullable element types
- When API compatibility with
List<T>is required - Managed heap scenarios where GC pressure is not a primary concern
- Cases where collection size cannot be statically bounded
Each collection type uses inline storage via the InlineArray8<T>, InlineArray16<T>, or InlineArray32<T> structs, which leverage the [InlineArray(N)] attribute to embed the specified number of elements directly inside the struct. This is a value-type collection stored on the stack (when not captured in a reference type).
- Inline storage: N elements stored as struct fields (where N โ {8, 16, 32})
- No heap allocation
ref structsemantics (no boxing, no reference storage)
- Unmanaged element types only (constraint:
T : unmanaged, IEquatable<T>) - Fixed capacity of exactly 32 elements
- Throws
InvalidOperationExceptionwhen capacity is exceeded - Value-type semantics: copies on assignment/parameter passing
A list with a maximum capacity of 8, 16, or 32 unmanaged elements respectively.
Key methods:
Add(T item)โ add to end; throws if fullTryAdd(T item)โ add to end; returns false if fullAddRange(ReadOnlySpan<T> items)โ bulk addInsert(int index, T item)โ insert at indexTryInsert(int index, T item)โ insert at index; returns false if invalid or fullRemove(T item)โ remove first occurrenceRemoveAt(int index)โ remove by indexT this[int index]โ indexer with ref return for in-place modificationSpan<T> AsSpan()โ get current elements as a spanContains(T item)โ linear searchClear()โ empty the listint Countโ get current element countconst int Capacityโ fixed maximum capacity (8, 16, or 32)
Example:
var list = new InlineList16<int>();
list.Add(1);
list.Add(2);
list.Add(3);
int value = list[0]; // 1
var span = list.AsSpan();
foreach (var item in span) {
Console.WriteLine(item);
}
foreach (var item in list) {
Console.WriteLine(item);
}LIFO (Last-In-First-Out) collection with maximum capacity of 8, 16, or 32 elements.
Key methods:
Push(T item)โ push to stack; throws if fullTryPush(T item)โ push; returns false if fullT Pop()โ pop and return; throws if emptybool TryPop(out T result)โ pop safelyref T Peek()โ return ref to top without removing; throws if emptySpan<T> AsSpan()โ get all elements (in insertion order)Clear()โ empty the stackint Countโ get current element countconst int Capacityโ fixed maximum capacity (8, 16, or 32)
Example:
var stack = new InlineStack16<int>();
stack.Push(10);
stack.Push(20);
stack.Push(30);
int top = stack.Pop(); // 30
int next = stack.Pop(); // 20
ref int peek = ref stack.Peek();
peek = 100; // modify in-place
foreach (var item in stack) {
Console.WriteLine(item); // Iterates in reverse order (LIFO)
}FIFO (First-In-First-Out) collection with maximum capacity of 8, 16, or 32 elements. Internally uses a circular buffer for O(1) enqueue/dequeue.
Key methods:
Enqueue(T item)โ add to back; throws if fullTryEnqueue(T item)โ add safely; returns false if fullT Dequeue()โ remove and return from front; throws if emptybool TryDequeue(out T result)โ dequeue safelyref T Peek()โ return ref to front without removing; throws if emptyClear()โ empty the queueint Countโ get current element countconst int Capacityโ fixed maximum capacity (8, 16, or 32)
Example:
var queue = new InlineQueue16<int>();
queue.Enqueue(1);
queue.Enqueue(2);
queue.Enqueue(3);
int first = queue.Dequeue(); // 1
int second = queue.Dequeue(); // 2
ref int front = ref queue.Peek();
front = 999; // modify in-place
foreach (var item in queue) {
Console.WriteLine(item); // Iterates in FIFO order
}- Fixed capacity: Exactly 8, 16, or 32 elements depending on collection variant; exceeding capacity throws
InvalidOperationException. - Unmanaged types only:
Tmust satisfyT : unmanaged, IEquatable<T>. - Value semantics: Assignment and parameter passing copy the entire struct.
- Struct size: Each collection is (capacity * sizeof(T)) bytes plus overhead. For example:
InlineList8<int>: 32 bytes + 4 bytes (count) = 36 bytesInlineList32<int>: 128 bytes + 4 bytes (count) = 132 bytes
โ ๏ธ Stack memory warning: Because storage is inline, using a large unmanaged element type can push the struct's stack footprint high, potentially leading to significant stack pressure or aStackOverflowException. Consider using smaller size variants, heap-based collections, or passing the struct byref/into avoid costly copies.- ref struct: Cannot be stored in fields of reference types or classes; cannot be boxed.
- No null elements: Elements must be valid unmanaged values.
- Exceptions:
InvalidOperationExceptionโ capacity exceeded or collection is empty (on Pop/Peek/Dequeue without Try- variant)ArgumentOutOfRangeExceptionโ invalid index (RemoveAt)
All operations are O(1) constant time except:
Remove(T item)โ O(n) linear search and shiftRemoveAt(int index)โ O(n) shifts remaining elementsInsert(int index, T item)โ O(n) shifts elements to the right
Indexer access (this[int index]) and Peek/Pop operations have zero bounds checking and are aggressively inlined.
For detailed benchmarks and comparisons with List<T>, Stack<T>, and Queue<T>, see docs/benchmarks.md.
- Architecture โ internal design and memory layout
- Design Philosophy โ principles and goals
- Memory Model โ stack vs heap, value semantics, ref safety
- Collections โ per-collection API reference and examples
- Performance โ benchmark methodology and results
- Limitations โ hard constraints and exceptions
- When to Use โ recommended scenarios
- When Not to Use โ scenarios to avoid
- FAQ โ common questions
BenchmarkDotNet results comparing InlineCollections with standard BCL collections are available in the benchmarks/ directory. Run:
dotnet run --project benchmarks/InlineCollections.Benchmarks -c ReleaseKey findings:
- Add operations: InlineList32 is 3-5x faster than List for small collections (zero allocations)
- Indexer access: Near-identical performance to List (both use direct memory access)
- Memory: Zero heap allocations vs one allocation for List
See docs/benchmarks.md for full results.
- Optimized for hot paths in high-performance systems
- Zero-allocation primitive for small collections
- Reference-type performance with stack allocation semantics
- Deterministic and cache-friendly
- A replacement for
List<T>,Stack<T>, orQueue<T> - A thread-safe or concurrent collection
- A general-purpose data structure for arbitrary workloads
- A garbage-collected or reference-type collection
Inline (InlineCollections):
- โ No allocation
- โ Cache-friendly
- โ Fast small operations
- โ Fixed capacity
- โ Struct copying cost
- โ Stack size cost
Heap (List):
- โ Dynamic capacity
- โ Unbounded growth
- โ No copying (reference type)
- โ Allocation overhead
- โ GC pressure
- โ Cache misses
Bounds-checked (List):
- โ Safe by default
- โ Branch misprediction overhead
Unchecked (InlineCollections):
- โ 0 branches in hot loops
- โ Faster indexing
- โ Caller must ensure valid indices (in practice, usually guaranteed)