I have some code that looks like this, in an unsafe context:
ValidatePartialOperation(array, startingOffset, runLength);
fixed (double* _op = array)
double* op = _op + startingOffset;
double* op = preCall(array, startingOffset, runLength);
using (double* op = preCall(array, startingOffset, runLength))
Just for future readers, I think I've pretty well figured it out, though with a few educated guesses.
Basically it seems that
fixed is the C# way to invoke the C++/CLI pin_ptr. These are special variables that must be declared as local variables on the stack. They don't seem to communicate with the GC directly, so they're very light weight. Instead, when the GC runs it's smart enough to scour the callstacks of all active threads and check to see if any functions' variables are these special pinning pointers. If they are, whatever they point at is marked as pinned and won't be moved in memory during garbage collection.
GCHandle.Alloc(obj, GCHandleType.Pinned) actually communicates with the GC and places the object in a list of objects not to be moved. Which means every time you
GCHandle.Alloc and then
Free, you're adding and removing elements to the list and doing work. Pinning pointers are an entirely passive mechanism and don't need to do any extra work. That also explains why you can't change the value of a fixed pointer: the managed object it points to is only guaranteed to be fixed as long as the fixed pointer points to it. And if the fixed pointer pointed to a different object, it would now be pinned instead. And if you set the fixed pointer to null, even for just a moment, it would no longer be pinning anything and all pointer math you've done so far would be invalidated.
Which explains the performance hit when I tried to switch to GCHandles. So
fixed isn't just the best tool, it's the only tool, at least when performance is important. Even if the syntax is sometimes awkward.