Taking a closer look at the CompilerServices Unsafe approach to unsafe reference arithmetic over fixed statement in .NET.
.NET has a garbage collector (GC) that automates memory management. This is incompatible with working with direct memory pointers, since the GC needs to track object usage and move objects around when required. You are of course free to allocate memory manually from the operating system and work on direct pointers, and as a friendly gesture you can tell the GC how much memory you allocate/de-allocate so that it knows when to respond to memory pressure.
You can however do reference arithmetic to access memory that is managed by the GC. This used to be done with fixed statements, but .NET Core with its many awesome improvements also offer a better way to do reference arithmetic.
Note that this kind of operation is considered “unsafe”, and even requires both compiler to allow unsafe code as well as unsafe keyword on class or method. The “unsafe” part of this comes from the fact that there are no bounds check.
From MSDN: In the common language runtime (CLR), unsafe code is referred to as unverifiable code. Unsafe code in C# is not necessarily dangerous; it is just code whose safety cannot be verified by the CLR. The CLR will therefore only execute unsafe code if it is in a fully trusted assembly. If you use unsafe code, it is your responsibility to ensure that your code does not introduce security risks or pointer errors.
Using fixed statement
This example will extract element i from array using fixed statement for unsafe memory pointer:
1 2 3 4 5 6 7 8 |
public unsafe static int GetFixed(int[] array, int i) { fixed (int* p = &array[0]) { *p = *(p + i); return *p; } } |
If we take a look at the X64 ASM output we see that this is fairly straight forward pointer arithmetic.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
; [rcx] is pointer to reference table in array ("int[] array" parameter) ; [rcx+8] is pointer to array size ; [rcx+16] is pointer to first element of array ; edx contains index ("int i" parameter) L0000: sub rsp, 0x28 ; Move stack pointer L0004: cmp dword ptr [rcx+8], 0 ; Check if "array" object reference is null (size is 0) L0008: jbe short L0027 ; If so, jump to exception ; This section sets rax pointer to first element of array L000a: add rcx, 0x10 ; Add 16 (decimal) to rcx pointer (first element of array) L000e: mov [rsp+0x20], rcx ; Copy pointer to first element of array to stack L0013: mov rax, [rsp+0x20] ; Copy decimal 16 in stack to rax L0018: movsxd rdx, edx ; Convert edx to 64-bit in rdx (value "i") L001b: mov edx, [rax+rdx*4] ; Get value in pointer to element number "i": ; [16+i*4] where 4 is size of int L001e: mov [rax], edx ; Copy result to address in [rax] L0020: mov eax, [rax] ; Convert result to 32-bit in eax (which will be returned) L0022: add rsp, 0x28 ; Move stack pointer back L0026: ret ; Return L0027: call 0x00007ffd6dd2ec10 ; Throw NullReferenceException L002c: int3 ; Signal breakpoint |
Using System.Runtime.CompilerServices.Unsafe
There is another method to achieve the same result, using System.Runtime.CompilerServices.Unsafe.
1 2 3 4 5 6 7 8 |
public unsafe static int GetUnsafe(int[] array, int i) { var p = Unsafe.AsPointer(ref array[0]); p = Unsafe.Add<int>(p, i); return Unsafe.Read<int>(p); // This would give same x64 ASM output: //return Unsafe.AsRef<int>(p); } |
Looking at the x64 ASM we see that it is a bit cleaner.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
; [rcx] is pointer to reference table in array ("int[] array" parameter) ; [rcx+8] is pointer to array size ; edx contains index ("int i" parameter) L0000: sub rsp, 0x28 ; Move stack pointer L0004: cmp dword ptr [rcx+8], 0 ; Check if "array" object reference is null (size is 0) L0008: jbe short L0019 ; If so, jump to exception L000a: add rcx, 0x10 ; Add 16 (decimal) to rcx pointer (first element of array) L000e: movsxd rax, edx ; Convert edx to 64-bit in rdx (value "i") L0011: mov eax, [rcx+rax*4] ; Get value in pointer to element number "i": ; [16+i*4] where 4 is size of int L0014: add rsp, 0x28 ; Move stack pointer back L0018: ret ; Return L0019: call 0x00007ffd6dd2ec10 ; Throw NullReferenceException L001e: int3 ; Signal breakpoint |
Implementation of Unsafe class
While fixed statement was a language feature handled by the compiler, the implementation of Unsafe class is done in IL. Implementation details can be seen here.
For example the two methods AsPointer and Read used in example above:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
.method public hidebysig static void* AsPointer<T>(!!T& 'value') cil managed aggressiveinlining { .custom instance void System.Runtime.Versioning.NonVersionableAttribute::.ctor() = ( 01 00 00 00 ) .maxstack 1 ldarg.0 conv.u ret } // end of method Unsafe::AsPointer .method public hidebysig static !!T Read<T>(void* source) cil managed aggressiveinlining { .custom instance void System.Runtime.Versioning.NonVersionableAttribute::.ctor() = ( 01 00 00 00 ) .maxstack 1 ldarg.0 ldobj !!T ret } // end of method Unsafe::Read |
Benchmarks
The difference between the two methods is negligible, and a benchmark is pointless. But why not, for fun. Less CPU instructions should translate to better performance. But the difference are so small and error margin is subject how it is used.
The benchmark summarizes 1 million integers from an int[] array with 1M items (4MB) using linear access. The time per item is just above 0.5ns (5×10-7 ms / 0.0000005 ms), somewhere around 1.800.000.000 operations per second.


Conclusion
Unsafe.* provides a way to work with reference pointers in managed memory using managed code. It removes the need for a fixed scope and allows for returning ref pointers to data types in memory.
The compiler output from using Unsafe.AsPointer and Unsafe.Add is cleaner than the fixed statement counterpart, but does not yield any measurable performance difference.