Jay Lemmon Jay Lemmon - 1 year ago 35
C# Question

What exactly is 'fixed' doing?

I have some code that looks like this, in an unsafe context:

ValidatePartialOperation(array, startingOffset, runLength);

fixed (double* _op = array)
double* op = _op + startingOffset;
callSomething(op, runLength);

And I have that copy+pasted in a couple of different places. But I hate having that kind of validation and pointer arithmetic in multiple places, so I'd like to combine together the logic in to a single line that looks something like:

double* op = preCall(array, startingOffset, runLength);
callSomething(op, runLength);

Or even better:

using (double* op = preCall(array, startingOffset, runLength))
callSomething(op, runLength);

But whatever happens I can't afford to lose performance from the 'fixed' version.

My plan right now is to mimic what the fixed statement is doing, but I don't actually know what that is. Presumably some try-catch block with a pinning operation?

Answer Source

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.

By comparison, 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.