C# fixed Statement: Pinning Memory for Pointers

The fixed statement in C# prevents the Garbage Collector (GC) from relocating a movable variable during runtime, providing a stable memory address required for direct pointer operations. In managed environments, the garbage collector periodically compacts memory, shifting objects to optimize space. By using fixed inside an unsafe context, developers can temporarily "pin" an object in place, safely obtaining a raw pointer to interact with native APIs, perform high-performance memory manipulation, or handle unmanaged buffers without risking memory corruption.

Why Memory Pinning Is Necessary

The .NET Garbage Collector uses a mark-and-sweep algorithm with compaction. During compaction, active objects residing on the managed heap are moved to contiguous memory locations to reduce fragmentation.

If you take the memory address of a managed object and the GC relocates that object immediately afterward, your pointer points to invalid memory or another unrelated object. The fixed statement resolves this issue by informing the runtime's execution engine that a specific managed object must remain at its exact physical address until the execution exits the fixed block.

How the fixed Statement Works

When a fixed block executes:

  1. Pinning: The runtime marks the object's header with a pin flag, instructing the GC's compaction phase to skip moving this particular instance.
  2. Pointer Initialization: A pointer variable declared within the fixed statement receives the exact physical address of the pinned data.
  3. Execution: Code inside the block executes with direct, unmanaged memory access via the pointer. The initialized pointer is read-only; you cannot reassign it to point elsewhere.
  4. Unpinning: Once execution leaves the scope of the fixed block, the pin flag is cleared. The GC is once again free to relocate or collect the object during its next cycle.

Syntax and Example

To use pointers and the fixed statement, your project must have unmanaged code enabled (<AllowUnsafeBlocks>true</AllowUnsafeBlocks> in the .csproj file).

unsafe
{
    byte[] buffer = new byte[] { 10, 20, 30, 40, 50 };

    // Pin the array in memory
    fixed (byte* pBuffer = buffer)
    {
        // pBuffer points directly to the first element (buffer[0])
        for (int i = 0; i < buffer.Length; i++)
        {
            *(pBuffer + i) += 5; // Directly modify memory
        }
    } // The array is automatically unpinned here
}

Supported Types for Pinning

The fixed statement works with several types of data structures:

  • Arrays: Pinning an array (e.g., fixed (int* p = array)) yields a pointer to its first element.
  • Strings: Pinning a string (e.g., fixed (char* p = str)) yields a pointer to the first character, making it easy to pass strings to unmanaged C-style APIs expecting a const wchar_t*.
  • Variables and Fields: Any value type variable or field residing on the heap can be pinned (e.g., fixed (int* p = &myClass.myInt)).
  • Pinnable Types: Any custom class or struct that implements a GetPinnableReference() method returning a ref to an unmanaged type can be used directly in a fixed statement.

Performance Considerations

While pinning is essential for interoperability and extreme performance optimization, it should be used sparingly and for the shortest duration possible. Pinned objects create "islands" in memory that the garbage collector cannot shift. If objects are pinned frequently or for long periods, it leads to managed heap fragmentation, degraded allocation performance, and premature out-of-memory conditions.