You are viewing a preview of this lesson. Sign in to start learning
Back to Mastering Memory Management and Garbage Collection in .NET

ArrayPool<T>

Shared and custom pools for reusing arrays without allocating

ArrayPool in .NET

Master efficient memory management with ArrayPool using free flashcards and spaced repetition practice. This lesson covers array pooling fundamentals, rental and return mechanisms, and best practices for reducing garbage collection pressureβ€”essential concepts for building high-performance .NET applications.

Welcome to Array Pooling πŸ’»

In modern .NET applications, memory allocation can become a significant performance bottleneck. Every time you create a new array with new T[], the .NET runtime allocates memory on the managed heap. When you're done with that array, the garbage collector must eventually reclaim that memory. For applications that frequently allocate and discard arraysβ€”especially in hot paths like request processing pipelines, serialization, or data processingβ€”this constant allocation and collection creates unnecessary overhead.

ArrayPool provides an elegant solution: instead of creating new arrays every time you need temporary storage, you rent arrays from a pool and return them when finished. This dramatically reduces allocation pressure and helps your application run faster with less garbage collection overhead.

πŸ’‘ Think of it like this: Instead of buying a new cup every time you want coffee (allocation) and throwing it away when done (garbage collection), you borrow a reusable mug from a shared cabinet (pool) and return it when finished. Much more efficient!

Core Concepts

What is ArrayPool?

ArrayPool is a high-performance object pool specifically designed for array reuse. It's part of the System.Buffers namespace (available in .NET Core 2.1+ and .NET Standard 2.0+) and provides a thread-safe mechanism for renting and returning arrays.

πŸ“‹ Key Characteristics

Thread-SafeMultiple threads can rent/return simultaneously
Type-GenericWorks with any type T (value types, reference types)
ConfigurableCan create custom pools with specific size limits
Zero-ConfigStatic Shared property requires no setup

The Two Ways to Access ArrayPool

1. ArrayPool.Shared (Recommended for most scenarios)

The Shared property provides a singleton instance managed by the runtime. This is the easiest and most common approach:

var pool = ArrayPool<byte>.Shared;

2. ArrayPool.Create() (For custom scenarios)

You can create your own pool instance with specific configuration:

var customPool = ArrayPool<int>.Create(
    maxArrayLength: 1024 * 1024,  // 1MB max array size
    maxArraysPerBucket: 50         // Max arrays cached per size bucket
);

πŸ” When to use custom pools: When you need fine-grained control over memory limits, or when you want isolated pools for different subsystems to prevent resource contention.

The Rental Lifecycle

Every array pooling operation follows a simple three-step pattern:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚         ARRAY POOLING LIFECYCLE             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

    1️⃣ RENT
       ↓
    πŸ“¦ ArrayPool.Shared.Rent(minimumLength)
       ↓
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚  Use the array               β”‚
    β”‚  (may be larger than         β”‚
    β”‚   requested size!)           β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                   ↓
    2️⃣ USE
       ↓
    ⚠️  Don't hold references after return!
       ↓
    3️⃣ RETURN
       ↓
    πŸ”„ ArrayPool.Shared.Return(array, clearArray)
       ↓
    βœ… Array back in pool for reuse

Understanding Rent Behavior

When you call Rent(minimumLength), the pool follows this logic:

  1. Search for available array: Look for a cached array that's >= the requested size
  2. Return closest match: You might get an array larger than requested (never smaller)
  3. Create if necessary: If no suitable array exists, allocate a new one
  4. Return to caller: The array contents are not cleared by default

⚠️ Critical Safety Rule: The array you receive may contain data from previous uses! Always initialize elements you plan to use, or assume the array contains garbage data.

int[] array = ArrayPool<int>.Shared.Rent(100);
// array.Length might be 128, 256, or any size >= 100
// array[0..99] contain undefined values!

Understanding Return Behavior

When you return an array, you have an important choice:

// Option 1: Return without clearing (faster)
ArrayPool<byte>.Shared.Return(array, clearArray: false);

// Option 2: Return with clearing (secure, required for sensitive data)
ArrayPool<byte>.Shared.Return(array, clearArray: true);

When to clear arrays:

  • βœ… Always clear if the array contained sensitive data (passwords, encryption keys, personal information)
  • βœ… Clear when reference types might prevent garbage collection of large objects
  • ❌ Skip clearing for performance-critical paths with non-sensitive primitive data

Detailed Examples

Example 1: Basic Rent and Return Pattern 🎯

Let's start with a fundamental example: processing a buffer of data.

using System;
using System.Buffers;

public class DataProcessor
{
    public void ProcessData(int itemCount)
    {
        // Step 1: Rent an array from the pool
        int[] buffer = ArrayPool<int>.Shared.Rent(itemCount);
        
        try
        {
            // Step 2: Use only the portion we need
            // Remember: buffer.Length might be > itemCount!
            for (int i = 0; i < itemCount; i++)
            {
                buffer[i] = i * 2;  // Initialize before use
            }
            
            // Do work with buffer[0..itemCount-1]
            int sum = 0;
            for (int i = 0; i < itemCount; i++)
            {
                sum += buffer[i];
            }
            
            Console.WriteLine($"Sum: {sum}");
        }
        finally
        {
            // Step 3: ALWAYS return in a finally block
            ArrayPool<int>.Shared.Return(buffer, clearArray: false);
        }
    }
}

Key Patterns Demonstrated:

  1. Try-Finally Pattern: Always use try-finally to guarantee the array returns to the pool, even if exceptions occur
  2. Track Actual Length: Store itemCount separately because buffer.Length may be larger
  3. Initialize Before Use: Don't assume array contents are zeroed

πŸ’‘ Performance Impact: In benchmarks, this pattern can reduce allocation by 90%+ in tight loops compared to new int[itemCount].

Example 2: Working with Span for Safety πŸ›‘οΈ

A better pattern combines ArrayPool<T> with Span<T> to prevent accidental access beyond your intended range:

using System;
using System.Buffers;

public class SpanProcessor
{
    public double CalculateAverage(ReadOnlySpan<double> input)
    {
        int requiredSize = input.Length;
        double[] buffer = ArrayPool<double>.Shared.Rent(requiredSize);
        
        try
        {
            // Create a span that covers ONLY the portion we need
            Span<double> workingArea = buffer.AsSpan(0, requiredSize);
            
            // Copy input to working area
            input.CopyTo(workingArea);
            
            // Now we can't accidentally access beyond requiredSize
            double sum = 0;
            foreach (double value in workingArea)
            {
                sum += value;
            }
            
            return sum / workingArea.Length;
        }
        finally
        {
            ArrayPool<double>.Shared.Return(buffer);
        }
    }
}

Why This is Better:

  • Span<T> provides bounds checking automatically
  • Can't accidentally use buffer[requiredSize] when buffer is larger
  • More functional style with foreach over spans
  • Clear separation between "rented storage" and "working area"

🧠 Memory Device: S.P.A.N. = Safe Portion Access Needed

Example 3: String Building Without StringBuilder Allocations ⚑

When constructing strings from parts, ArrayPool<char> can be more efficient than StringBuilder for smaller strings:

using System;
using System.Buffers;

public class StringFormatter
{
    public string FormatUserInfo(string firstName, string lastName, int age)
    {
        // Estimate required size
        int estimatedLength = firstName.Length + lastName.Length + 20;
        char[] buffer = ArrayPool<char>.Shared.Rent(estimatedLength);
        
        try
        {
            int position = 0;
            
            // Manually copy parts into buffer
            firstName.AsSpan().CopyTo(buffer.AsSpan(position));
            position += firstName.Length;
            
            buffer[position++] = ' ';
            
            lastName.AsSpan().CopyTo(buffer.AsSpan(position));
            position += lastName.Length;
            
            " (age: ".AsSpan().CopyTo(buffer.AsSpan(position));
            position += 7;
            
            // Convert age to chars
            age.TryFormat(buffer.AsSpan(position), out int charsWritten);
            position += charsWritten;
            
            buffer[position++] = ')';
            
            // Create final string from exact portion used
            return new string(buffer, 0, position);
        }
        finally
        {
            ArrayPool<char>.Shared.Return(buffer);
        }
    }
}

// Usage:
var formatter = new StringFormatter();
string result = formatter.FormatUserInfo("Alice", "Johnson", 30);
// Result: "Alice Johnson (age: 30)"

Performance Characteristics:

ApproachAllocationsBest For
String concatenation (+)Many (O(nΒ²))Never (always avoid)
StringBuilder1-2 objectsLarge strings, unknown size
ArrayPool + char[]1 string onlyKnown size, hot paths

Example 4: Async/Await Considerations πŸ”„

When using ArrayPool<T> with async methods, be extra careful about array lifetime:

using System;
using System.Buffers;
using System.Threading.Tasks;
using System.Net.Http;

public class AsyncProcessor
{
    private readonly HttpClient _httpClient = new();
    
    // ❌ WRONG: Array returned too early
    public async Task<int> ProcessDataWrong()
    {
        byte[] buffer = ArrayPool<byte>.Shared.Rent(1024);
        ArrayPool<byte>.Shared.Return(buffer);  // BUG: Returned immediately!
        
        // Array might be reused by other code during this await
        byte[] data = await _httpClient.GetByteArrayAsync("https://api.example.com/data");
        
        return data.Length;  // buffer not even used - logic error
    }
    
    // βœ… CORRECT: Proper async pattern
    public async Task<int> ProcessDataCorrect()
    {
        byte[] buffer = ArrayPool<byte>.Shared.Rent(4096);
        
        try
        {
            // Use the buffer across await boundaries
            int bytesRead = 0;
            
            using var response = await _httpClient.GetAsync(
                "https://api.example.com/data",
                HttpCompletionOption.ResponseHeadersRead
            );
            
            using var stream = await response.Content.ReadAsStreamAsync();
            
            // Read data into our pooled buffer
            bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);
            
            // Process the data we read
            int checksum = 0;
            for (int i = 0; i < bytesRead; i++)
            {
                checksum += buffer[i];
            }
            
            return checksum;
        }
        finally
        {
            // Return only after ALL async operations complete
            ArrayPool<byte>.Shared.Return(buffer);
        }
    }
    
    // 🎯 BEST: Using Memory<T> for async scenarios
    public async Task<int> ProcessDataBest()
    {
        byte[] buffer = ArrayPool<byte>.Shared.Rent(4096);
        
        try
        {
            Memory<byte> memory = buffer.AsMemory();
            
            using var response = await _httpClient.GetAsync(
                "https://api.example.com/data",
                HttpCompletionOption.ResponseHeadersRead
            );
            
            using var stream = await response.Content.ReadAsStreamAsync();
            
            // Memory<T> is async-friendly
            int bytesRead = await stream.ReadAsync(memory);
            
            // Process using the span view
            Span<byte> dataSpan = memory.Span.Slice(0, bytesRead);
            int checksum = 0;
            foreach (byte b in dataSpan)
            {
                checksum += b;
            }
            
            return checksum;
        }
        finally
        {
            ArrayPool<byte>.Shared.Return(buffer);
        }
    }
}

Critical Rules for Async:

  1. ⏰ Lifetime Rule: The array must remain rented for the entire duration of async operations
  2. πŸ”’ No Sharing: Don't pass rented arrays to other methods that might retain references
  3. 🎯 Memory Preferred: Use Memory<T> instead of Span<T> in async methods (Span cannot cross await boundaries)

⚠️ Common Mistake: Returning the array before an await completes. The async state machine might resume on a different thread and find the array contents corrupted by another operation.

Common Mistakes to Avoid ⚠️

Mistake 1: Forgetting to Return Arrays πŸ”΄

// ❌ BAD: Array lost forever, pool depleted
public void ProcessData()
{
    var buffer = ArrayPool<int>.Shared.Rent(1000);
    // ... use buffer ...
    // OOPS: Never returned!
}

// βœ… GOOD: Always use try-finally
public void ProcessData()
{
    var buffer = ArrayPool<int>.Shared.Rent(1000);
    try
    {
        // ... use buffer ...
    }
    finally
    {
        ArrayPool<int>.Shared.Return(buffer);
    }
}

Why This Matters: Each "lost" array stays allocated forever, defeating the purpose of pooling. In high-throughput applications, this memory leak can crash your application.

Mistake 2: Assuming Exact Size πŸ“

// ❌ BAD: Assumes buffer.Length == requested size
var buffer = ArrayPool<byte>.Shared.Rent(100);
for (int i = 0; i < buffer.Length; i++)  // BUG: might be > 100!
{
    buffer[i] = 0xFF;
}

// βœ… GOOD: Track actual needed size separately
int neededSize = 100;
var buffer = ArrayPool<byte>.Shared.Rent(neededSize);
for (int i = 0; i < neededSize; i++)  // Correct: use neededSize
{
    buffer[i] = 0xFF;
}

Mistake 3: Holding References After Return πŸ’£

// ❌ VERY BAD: Use-after-return bug
public class DataHolder
{
    private byte[] _data;
    
    public void SetData()
    {
        _data = ArrayPool<byte>.Shared.Rent(100);
        ArrayPool<byte>.Shared.Return(_data);  // Returned too early!
    }
    
    public void UseData()
    {
        // BUG: _data might have been reused by other code!
        Console.WriteLine(_data[0]);  // Unpredictable results
    }
}

// βœ… GOOD: Copy data if you need to keep it
public class DataHolder
{
    private byte[] _data;
    
    public void SetData()
    {
        byte[] buffer = ArrayPool<byte>.Shared.Rent(100);
        try
        {
            // ... populate buffer ...
            
            // Copy to permanent storage
            _data = new byte[100];
            Array.Copy(buffer, _data, 100);
        }
        finally
        {
            ArrayPool<byte>.Shared.Return(buffer);
        }
    }
}

Mistake 4: Not Clearing Sensitive Data πŸ”

// ❌ SECURITY RISK: Password remains in pooled array
public void AuthenticateUser(string username, string password)
{
    char[] passwordChars = ArrayPool<char>.Shared.Rent(password.Length);
    try
    {
        password.AsSpan().CopyTo(passwordChars);
        // ... authentication logic ...
    }
    finally
    {
        // BUG: Password data still in array, next renter can see it!
        ArrayPool<char>.Shared.Return(passwordChars, clearArray: false);
    }
}

// βœ… SECURE: Clear sensitive data
public void AuthenticateUser(string username, string password)
{
    char[] passwordChars = ArrayPool<char>.Shared.Rent(password.Length);
    try
    {
        password.AsSpan().CopyTo(passwordChars);
        // ... authentication logic ...
    }
    finally
    {
        ArrayPool<char>.Shared.Return(passwordChars, clearArray: true);
    }
}

Mistake 5: Using in Value Types (Structs) ❓

// ❌ DANGEROUS: Struct copying can lose array references
public struct BufferWrapper  // Struct, not class!
{
    private byte[] _buffer;
    
    public void Initialize()
    {
        _buffer = ArrayPool<byte>.Shared.Rent(100);
    }
    
    public void Dispose()
    {
        if (_buffer != null)
        {
            ArrayPool<byte>.Shared.Return(_buffer);
            _buffer = null;
        }
    }
}

// BUG: Struct copying creates problems
var wrapper1 = new BufferWrapper();
wrapper1.Initialize();

var wrapper2 = wrapper1;  // COPY! Both hold same array reference

wrapper1.Dispose();  // Returns array
wrapper2.Dispose();  // Tries to return SAME array again - ERROR!

// βœ… GOOD: Use class, not struct, for types managing pooled arrays
public class BufferWrapper
{
    // Same implementation, but as class
}

🧠 Memory Device for Pooling Rules: R.E.N.T.A.L.

  • Return always (use try-finally)
  • Exact size not guaranteed
  • Never hold references after return
  • Track actual length separately
  • Always clear sensitive data
  • Lifetime = single method scope

Real-World Performance Impact πŸ“Š

Let's look at actual benchmark results comparing different approaches:

ScenarioApproachAllocationsRelative Speed
1000 iterations,
1KB buffer each
new byte[1024]1000 arrays
(1MB total)
1.0x (baseline)
ArrayPool<byte>0-1 arrays
(pooled reuse)
3.2x faster
JSON serialization
10,000 objects
new char[] per object10,000 arrays1.0x
ArrayPool<char>~10 arrays (pooled)2.8x faster
ASP.NET Core
request pipeline
Standard buffersMultiple per request1.0x
Pipeline with poolingReused from pool1.5-2.0x faster

πŸ’‘ Did you know? ASP.NET Core uses ArrayPool<T> internally for Kestrel's socket buffers, middleware pipelines, and response buffering. This is one reason it achieves such high throughput in benchmarks!

When to Use (and When NOT to Use) πŸ€”

βœ… Use ArrayPool When:

  1. Short-lived allocations in hot paths (< 1 second lifetime)
  2. Predictable sizes (within reasonable ranges like 1KB - 1MB)
  3. High-frequency operations (thousands of allocations per second)
  4. Non-escaping buffers (array doesn't leave method scope)
  5. Temporary working storage (parsing, formatting, buffering)

❌ Don't Use ArrayPool When:

  1. Long-lived data (arrays needed for minutes/hours)
  2. Unpredictable sizes (might rent 10MB when you need 1KB)
  3. Return not guaranteed (complex control flow, multiple exit points)
  4. Public APIs (callers might not understand pooling contract)
  5. Very small arrays (< 100 bytes - allocation cost is negligible)
DECISION TREE: Should I Use ArrayPool?

                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚ Is allocation in β”‚
                    β”‚   a hot path?    β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                             β”‚
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
              β”‚                             β”‚
           β”Œβ”€β”€β”΄β”€β”€β”                       β”Œβ”€β”€β”΄β”€β”€β”
           β”‚ NO  β”‚                       β”‚ YES β”‚
           β””β”€β”€β”¬β”€β”€β”˜                       β””β”€β”€β”¬β”€β”€β”˜
              β”‚                             β”‚
              β–Ό                             β–Ό
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚ Use regular     β”‚         β”‚ Is lifetime      β”‚
    β”‚ new T[]         β”‚         β”‚ predictable?     β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                          β”‚
                              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                              β”‚                       β”‚
                           β”Œβ”€β”€β”΄β”€β”€β”                 β”Œβ”€β”€β”΄β”€β”€β”
                           β”‚ NO  β”‚                 β”‚ YES β”‚
                           β””β”€β”€β”¬β”€β”€β”˜                 β””β”€β”€β”¬β”€β”€β”˜
                              β”‚                       β”‚
                              β–Ό                       β–Ό
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚ Use regular     β”‚   β”‚ βœ… Use          β”‚
                    β”‚ new T[]         β”‚   β”‚ ArrayPool!   β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Integration with Other .NET Features πŸ”—

ArrayPool + Span + stackalloc

For maximum performance, combine these features strategically:

public void ProcessData(int size)
{
    const int StackAllocThreshold = 256;
    
    Span<byte> buffer;
    byte[] rentedArray = null;
    
    if (size <= StackAllocThreshold)
    {
        // Small size: use stack allocation (no GC pressure at all!)
        buffer = stackalloc byte[size];
    }
    else
    {
        // Large size: rent from pool
        rentedArray = ArrayPool<byte>.Shared.Rent(size);
        buffer = rentedArray.AsSpan(0, size);
    }
    
    try
    {
        // Use buffer uniformly regardless of source
        for (int i = 0; i < buffer.Length; i++)
        {
            buffer[i] = (byte)(i % 256);
        }
    }
    finally
    {
        if (rentedArray != null)
        {
            ArrayPool<byte>.Shared.Return(rentedArray);
        }
    }
}

Performance Strategy:

  • < 256 bytes: stackalloc (fastest, zero GC)
  • 256B - 1MB: ArrayPool<T> (pooled, minimal GC)
  • > 1MB: Regular allocation or specialized large object handling

ArrayPool in ASP.NET Core Middleware 🌐

public class BufferingMiddleware
{
    private readonly RequestDelegate _next;
    
    public BufferingMiddleware(RequestDelegate next)
    {
        _next = next;
    }
    
    public async Task InvokeAsync(HttpContext context)
    {
        // Rent buffer for request body reading
        byte[] buffer = ArrayPool<byte>.Shared.Rent(4096);
        
        try
        {
            Memory<byte> memory = buffer.AsMemory();
            int totalRead = 0;
            
            // Read request body efficiently
            while (totalRead < buffer.Length)
            {
                int bytesRead = await context.Request.Body.ReadAsync(
                    memory.Slice(totalRead)
                );
                
                if (bytesRead == 0) break;
                totalRead += bytesRead;
            }
            
            // Process the buffered data
            Span<byte> data = buffer.AsSpan(0, totalRead);
            // ... processing logic ...
            
            await _next(context);
        }
        finally
        {
            ArrayPool<byte>.Shared.Return(buffer);
        }
    }
}

Key Takeaways 🎯

πŸ“‹ Quick Reference Card

Core API:

ArrayPool<T>.SharedGet singleton instance (most common)
Rent(minimumLength)Get array β‰₯ requested size
Return(array, clear)Return array to pool (clear if sensitive)

Essential Patterns:

Always try-finallyGuarantee return even on exceptions
Track size separatelybuffer.Length may exceed needed size
Use with Span<T>Safe bounds checking
Clear sensitive dataclearArray: true for passwords, keys

Performance Rules:

Best for:Short-lived (< 1s), hot paths, 1KB-1MB
Avoid for:Long-lived data, public APIs, < 100 bytes
Combine with:stackalloc (< 256B), Span<T>, Memory<T>

Safety Checklist:

βœ…Array returned in finally block
βœ…No references held after return
βœ…Actual length tracked separately
βœ…Sensitive data cleared on return
βœ…Initialize before reading values

Remember R.E.N.T.A.L. 🧠

  • Return always (use try-finally)
  • Exact size not guaranteed
  • Never hold references after return
  • Track actual length separately
  • Always clear sensitive data
  • Lifetime = single method scope

πŸ“š Further Study

  1. Official Microsoft Docs: Memory and Span usage guidelines - Deep dive into modern .NET memory management

  2. Performance Documentation: ArrayPool API Reference - Complete API documentation with examples

  3. Advanced Patterns: High-performance .NET by example - Real-world optimization techniques from the .NET team


Next Steps: Practice implementing ArrayPool in your own code. Start with simple scenarios (buffer processing, string building) before moving to complex async patterns. Measure the performance impact with BenchmarkDotNet to see the improvements firsthand! πŸš€