● Interactive guide · Updated October 2026

How arrays really work
in C# and .NET 10

From new int[5] all the way down to cache lines and CPU registers. Play with simulators, see the memory, and learn exactly when an array is the right tool, and when it isn't.

O(1)time to read any element by index
64 Btypical x64 cache line: 16 ints at once
85,000 Barrays this big go to the Large Object Heap
~100×slower to fetch from RAM than from L1 cache
Chapter 1

Why arrays matter

An array is a fixed number of same-typed values stored side by side in one block of memory. That single idea makes it the fastest general-purpose container in .NET and the building block under List<T>, string, Dictionary, Span<T> and almost everything else.

🏨 The hotel-corridor analogy

Press play to walk through it.

🏗️
Built in one goAll rooms are built together in a single corridor: one memory block.
🔢
Numbered from 0Room 0, 1, 2… The number is the index.
🚶
Walk straight thereRoom 7 is "start + 7 rooms". No searching: O(1).
👀
Neighbours are closeVisiting rooms in order is fast: the CPU grabs a whole floor (cache line).
🧱
Can't add roomsNeed more? Build a bigger corridor and move everyone (copy).
Five ideas cover 90% of array behaviour.

⚡ Strengths

Instant index access, tiny overhead (24 bytes per array on 64-bit), best possible cache use, SIMD-friendly, zero-copy views with Span<T>.

⚠️ Weaknesses

Size is fixed at creation. Inserting or removing in the middle means shifting elements. Searching unsorted data is O(n). Arrays are always mutable.

🌍 Everywhere

Image pixels, audio samples, network buffers, game entities, matrices, lookup tables, ML embeddings, and the storage behind List<T>.

Chapter 2

From your code to the silicon

When you write arr[i] it passes through seven layers before a value reaches the CPU. Tap a layer to see what it does, how big it is, who owns it and how much you control.

Chapter 3 · Simulator

What an array looks like in memory

Every .NET array is an object on the managed heap. On 64-bit it starts with an 8-byte object header, an 8-byte MethodTable pointer (its type), a 4-byte Length plus 4 bytes of padding, then the elements packed back to back. Each row below is one 64-byte cache line.

Object header (8 B) MethodTable ptr (8 B) Length (4 B) Padding Elements

Tap any coloured byte to see its address formula.

Assumes the object starts on a cache-line boundary, which .NET does not guarantee (objects are 8-byte aligned). Layout shown is CoreCLR x64/Arm64; it's an implementation detail and can change.

🧮 Size calculator

Chapter 4

How long things take

Arrays are fast because they cooperate with the hardware. These are typical, rounded figures for a modern desktop/server CPU (they vary a lot by hardware, clock speed and load). The bar length uses a log scale.

The lesson: one trip to RAM costs as much as ~100 cache hits. Arrays win because a single trip brings in 64 bytes of neighbours, and the hardware prefetcher fetches the next lines before you ask.

Chapter 5

Core concepts, one at a time

Each tab is one idea with a tiny hands-on demo. Start on the left and work right.

Creating arrays

Every new array is zero-initialised: numbers become 0, bool becomes false, references become null. The length is fixed for its whole life.

Pick a line of code above.

Indexing is just arithmetic

To find arr[i] the CPU computes address = arrayRef + 16 + i × sizeof(T) in a single instruction. That's why access time doesn't depend on the array's size. C# 8 added from-end indices: arr[^1] means arr[arr.Length - 1].


  

Bounds checks keep you safe

Unlike C, .NET checks every index. Reading arr[8] on an 8-element array throws IndexOutOfRangeException instead of reading random memory. The check is one compare-and-branch, and the JIT removes it when it can prove the index is valid (see Chapter 8).

Value types are stored inline, reference types are stored as pointers

An int[] or Point[] (struct) holds the actual values in the block. A string[] or Customer[] (class) holds 8-byte references; the objects live elsewhere on the heap. That extra hop costs a possible cache miss per element.

Assigning an array copies the reference, not the data

Arrays are reference types. b = a makes two names for one block. To get independent data you must copy.

Choose how b is created, then mutate a.

Rectangular int[,] vs jagged int[][]

A multi-dimensional array is one block with extra per-dimension lengths in its header. A jagged array is an array of references to separate row arrays, which may have different lengths. In .NET the jagged form is usually faster (the JIT optimises 1-D access better) and analyzer rule CA1814 suggests it when rows differ in size; the rectangular form is one allocation and simpler for true grids.

Ranges: copy vs view

arr[start..end] on an array allocates a new array and copies. arr.AsSpan(start..end) creates a Span view over the same memory: no copy, no allocation.

Array covariance: an old trap

C# lets a string[] be used where an object[] is expected. To stay type-safe the runtime must check every store into a reference-type array, and a wrong store throws ArrayTypeMismatchException. This check is also a small hidden cost; sealed element types and value types avoid most of it.


    

Arrays can't grow: "resizing" means copying

Array.Resize(ref arr, n) allocates a new array, copies the old elements and points your variable at it. The old array becomes garbage. Doing this once per added item is O(n²); List<T> doubles capacity instead (Chapter 9).

Chapter 6

The state of the art (October 2026)

.NET 10 (November 2025, Long-Term Support) with C# 14 is the current LTS. .NET 11 with C# 15 reached Release Candidate 1 with a go-live licence in September 2026; general availability is planned at .NET Conf, 10–12 November 2026. Here's how array-related features arrived.

FeatureSinceWhat it does for arrays
ArrayPool<T>.NET Core 1.0 (System.Buffers)Rent and return reusable arrays instead of allocating.
Span<T>, Memory<T>C# 7.2 · .NET Core 2.1Safe, zero-copy views over arrays, stack memory or native memory.
Indices ^1 and ranges a..bC# 8 · .NET Core 3.0From-end access and slicing syntax.
Pinned Object Heap, GC.AllocateUninitializedArray.NET 5Allocate pinned buffers for interop; skip zeroing when you overwrite everything.
Array.MaxLength.NET 6The real upper bound on element count (0x7FFFFFC7).
List patterns, u8 literalsC# 11 · .NET 7arr is [1, .., var last]; "GET"u8 is a ReadOnlySpan<byte> with no allocation.
Vector512<T> / AVX-512, TensorPrimitives, Frozen collections.NET 8Process 16 floats per instruction; ready-made SIMD maths over spans; fast read-only lookups.
Collection expressions [1, 2, ..rest], [InlineArray]C# 12 · .NET 8One syntax for arrays, lists and spans; fixed-size arrays embedded inside structs.
params ReadOnlySpan<T> (params collections)C# 13 · .NET 9Variadic methods without allocating a hidden array.
Loop optimisations (induction-variable widening, strength reduction).NET 9 JITTighter array loops.
First-class span conversionsC# 14 · .NET 10Arrays convert implicitly to Span<T>/ReadOnlySpan<T>, even for extension methods and generic inference.
Stack-allocated small arrays, array-interface devirtualisation.NET 10 JITSmall fixed-size arrays that never escape a method can live on the stack; foreach over an array seen as IEnumerable<T> gets much cheaper.
Collection expression arguments [with(capacity: 100), ..]C# 15 · .NET 11 (RC)Pass constructor args (capacity, comparer). Not supported for arrays or spans.
Higher CPU baseline (x86-64-v2).NET 11 (RC)SSE4.2/POPCNT guaranteed; ReadyToRun code targets x86-64-v3 (AVX2) on Windows and Linux.
Operation on T[]CostWhy
Read / write arr[i]O(1)One address calculation and one load/store.
arr.LengthO(1)Stored in the object at offset +8.
Linear search (IndexOf, Contains)O(n)Checks each element, but vectorised for primitives.
Array.BinarySearch (sorted)O(log n)Halves the range each step.
Array.SortO(n log n)Introspective sort, in place, not stable.
Insert / remove in the middleO(n)Must shift later elements, or copy into a new array.
"Append" (Array.Resize)O(n)New array + full copy every time.
new T[n]O(n)The memory must be zeroed.
Chapter 7 · Simulator

Why access order matters: the cache-line simulator

Memory below is a long[128] drawn as 16 cache lines of 8 elements (64 bytes each). The tiny model "L1 cache" holds 4 lines and evicts the least-recently-used. A hit costs ~1 ns, a miss ~80 ns. Same data, same work, very different speed.

L1 cache (4 lines, LRU)

Real-world rule: walk arrays front to back, walk 2-D data in the order it is stored (row by row in C#), keep hot data in contiguous arrays of structs or separate arrays, and avoid chasing pointers when performance matters.

Chapter 8 · Lab

The JIT, bounds checks and how to get them removed

The JIT (just-in-time compiler, RyuJIT) turns your IL into machine code at run time. With tiered compilation and Dynamic PGO (profile-guided optimisation, on by default since .NET 8) hot loops are recompiled with full optimisation. One optimisation that matters for arrays is bounds-check elimination: if the JIT can prove 0 ≤ i < Length, the check disappears.


        

Simplified x64 loop body


        

Assembly is simplified for teaching. Check the real output with DOTNET_JitDisasm="Sum" (.NET 7+) or BenchmarkDotNet's [DisassemblyDiagnoser]. Exact results depend on .NET version, tier and surrounding code.

🧠 Why a check is cheap

It's one cmp + one branch the CPU predicts correctly almost every time. Note the unsigned compare (jae): it rejects negative indices in the same instruction.

🐢 Why it still matters

In tight loops a remaining check can block vectorisation and loop unrolling, so the real loss can be 2× or more, not just one instruction.

🧰 Tricks

Loop to arr.Length or span.Length; copy fields to locals; prefer foreach/Span; as a last resort use MemoryMarshal.GetArrayDataReference + Unsafe.Add (no checks, you own the risk).

Chapter 9 · Simulator

How List<T> fakes a growable array

List<T> wraps a private T[]. When it's full, it allocates an array twice as big (starting at 4), copies everything over and drops the old one. That makes Add amortised O(1), but each growth is a full copy and creates garbage.

Takeaway: if you know (or can estimate) the final count, pass it as the capacity, or build a T[] directly. Use CollectionsMarshal.AsSpan(list) to loop over a list's backing array without the enumerator (don't add/remove while holding that span).

Chapter 10

What you control vs what's decided for you

You can't pick a memory address or tell the CPU what to cache, but your choices decide almost everything that matters for performance.

✅ You decide

  • Element type: struct (inline) vs class (pointer per element)
  • Size, and whether it's known up front
  • Data shape: array of structs vs struct of arrays
  • Where it lives: heap, pool, stackalloc, [InlineArray], native memory
  • Access order and loop shape (affects bounds checks and SIMD)
  • Whether to copy (ToArray, ranges) or view (Span)
  • Pinning for interop: fixed or GC.AllocateArray(pinned: true)
  • Explicit SIMD: Vector<T>, Vector128/256/512, TensorPrimitives

⚙️ Decided for you

  • Exact address and alignment (8-byte aligned objects)
  • Zeroing of new memory (unless you ask for uninitialized)
  • SOH vs LOH placement (≥ 85,000 bytes ⇒ LOH)
  • When the GC runs, and whether it moves (compacts) your array
  • Which bounds checks the JIT can remove; tiering and PGO
  • Whether a small local array gets stack-allocated (.NET 10 escape analysis)
  • Cache sizes, cache-line size, prefetching, branch prediction
  • Which SIMD width is hardware-accelerated on the machine
Chapter 12

When to use an array, and when not to

Arrays are the default for fixed-size, index-based, hot-path data. For everything else .NET has a better-shaped container. Answer the questions to get a recommendation, then check the comparison table.

🧭 Decision helper

TypeSizeIndex O(1)Lookup by keyInsert middleHeap allocAcross await / in fieldsBest for
T[]Fixed✅❌ O(n)❌ O(n)Yes (maybe stack in .NET 10)✅Known-size data, hot loops, buffers
List<T>Growable✅❌ O(n)⚠️ O(n)Yes (+ regrowth)✅Unknown count, general purpose
Span<T> / ReadOnlySpan<T>View✅❌❌Never (it's a view)❌ stack onlySlicing, parsing, sync hot paths
Memory<T>Viewvia .Span❌❌No✅Async I/O buffers
ArrayPool<T> rent≥ requested✅❌❌Reused✅ (return it!)Temporary large buffers
stackallocFixed, small✅❌❌No (stack)❌Tiny scratch buffers (< ~1 KB)
[InlineArray(N)] structFixed, compile-time✅❌❌Inside its container✅Fixed buffers inside structs, interop
ImmutableArray<T>Fixed✅❌New copyYes✅Shared read-only data, thread-safe
Dictionary<K,V> / HashSet<T>Growable❌✅ O(1) avgn/aYes✅Lookups by key, uniqueness
FrozenDictionary / FrozenSetFixed❌✅ fastest reads❌Yes (built once)✅Read-mostly lookup tables
LinkedList<T>Growable❌ O(n)❌✅ O(1) at a nodePer node✅Rare: frequent splice with node handles
Queue<T> / Stack<T>Growable❌❌Ends onlyYes (array-backed)✅FIFO / LIFO work lists
Chapter 13

Modern .NET APIs for arrays

These tools let you keep array-level speed while avoiding allocations and copies. All examples target .NET 10 / C# 14; notes mark where something needs a newer or older version.

One syntax for arrays, lists and spans. The compiler picks the cheapest construction for the target type: constant ReadOnlySpan<byte> data is baked into the assembly, and a Span<T> target may use the stack.

A Span<T> is just a reference + a length (two registers). Slicing it is free. It's a ref struct: it can't be boxed, stored in a class field, or used across await; use Memory<T> there.

Rent may return a larger array than you asked for, and its contents are not cleared. Always track your own length and return in finally.

stackalloc carves memory out of the current thread's stack (often 1 MB by default on Windows, more on Linux). Keep it small and never inside an unbounded loop.

[InlineArray(N)] (C# 12) makes a struct whose single field is repeated N times, giving a real fixed-size array inside another object or on the stack with indexing, foreach and span conversion.

SIMD (single instruction, multiple data) processes several elements per instruction: 8 ints with AVX2 (256-bit), 16 with AVX-512. Contiguous arrays are what make this possible. Prefer TensorPrimitives or built-in vectorised methods before writing your own.

The Array class and MemoryExtensions already contain vectorised, well-tested helpers. Reach for them before writing loops.

For the rare hot loop where a profiler proves bounds checks matter. ❌ Wrong indices here corrupt memory silently, so wrap it in a well-tested method.

Chapter 14

Proven array patterns

Five shapes you'll see in fast .NET code. Watch the dot travel through each flow.

Chapter 15

Measure, don't guess: BenchmarkDotNet

BenchmarkDotNet (0.15.x is current in 2026; recent versions add Roslyn analyzers that catch mistakes) handles warm-up, JIT tiering, statistics and allocation counting for you. Never time array code with a single Stopwatch run in Debug mode.

📊 How to read the results

Illustrative shape of results for summing 10,000 ints on a modern x64 machine. Your numbers will differ; the ratios are what to look at.

Mean
Average time per call
Ratio
Relative to the [Benchmark(Baseline = true)] method
Allocated
Managed bytes allocated per call (from [MemoryDiagnoser])
Gen0
Gen 0 collections per 1,000 calls

✅ Do

  • Run dotnet run -c Release
  • Add [MemoryDiagnoser]
  • Use [Params] for several sizes (fits-in-cache vs not)
  • Return results so work isn't optimised away

❌ Don't

  • Benchmark with a debugger attached
  • Allocate test data inside the benchmark method
  • Trust one run or one machine
  • Compare across .NET versions without saying so

🔎 Other tools

  • dotnet-counters for live GC/LOH stats
  • dotnet-trace + PerfView / VS profiler for allocations
  • [DisassemblyDiagnoser] to see bounds checks
  • Object layout via the SOS !dumpobj command
Chapter 16

Common problems and their fixes

The mistakes that show up again and again in code reviews and production incidents.

Chapter 17 · Applied simulator

Fix the slow upload endpoint

An ASP.NET Core endpoint receives ~1 MB CSV uploads, parses them, computes a grid of totals and a checksum. It's slow and has GC spikes. Turn on fixes and run 1,000 requests to see the effect. Numbers come from a simplified teaching model, not a real benchmark.

🛠 Fixes

📈 Results

Chapter 18

Real-world scenarios

Situations you'll meet in real projects and what helps. Filter by area.

Chapter 19

Cheat sheet

Everything important on one page.

Chapter 20

Glossary

Every term used in this guide, searchable.