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:
- Pinning: The runtime marks the object's header with a pin flag, instructing the GC's compaction phase to skip moving this particular instance.
- Pointer Initialization: A pointer variable declared
within the
fixedstatement receives the exact physical address of the pinned data. - 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.
- Unpinning: Once execution leaves the scope of the
fixedblock, 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 aconst 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 arefto an unmanaged type can be used directly in afixedstatement.
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.