Introduction
Having written an article on interior pointers, I thought the next logical topic for an article from me should be pinning pointers. This article will explain what pinning pointers are, when they come in handy and how they can be harmful to the overall performance of your .NET application. If you haven't already read my article on interior pointers and if you don't know what interior pointers are, then it'd be a good idea to quickly go through that article :-
Now that I've enjoyed the rather conceited satisfaction of having plugged my own article, lets get going.
What are pinning pointers?
During Garbage Collection cycles, the GC frequently compacts the CLI heap, relocates objects and adjusts managed references so that they continue referencing the same object that they were before the GC cycle. Since this is done transparently, the programmer needn't really be bothered with this at all unless there is an unmanaged pointer pointing to a CLI heap object. Since the GC cannot adjust unmanaged pointers, this would obviously result in havoc and so unmanaged pointers are not allowed to point to CLI objects. But there might be situations where you want to pass a CLI object to an unmanaged function (possibly during native interop) and to facilitate this, the CLR provides us with pinning pointers. A pinning pointer (or a pinned pointer) prevents the GC from relocating the pinned object for as long as the pinning pointer remains in scope. It's not difficult to write some arguably contorted code to verify this and we'll do just that :-
See below our test ref
class, the DoLotsOfAllocs
function that we use to fill up the CLI heap and to force a GC cycle, and a native function that takes an unmanaged int*
as argument.
ref class Test
{
public:
int m_i;
};
void DoLotsOfAllocs(int x=10000)
{
for(int i=0; i<x; i++)
{
gcnew Test();
}
}
void NativeFunc(int* pInt)
{
*pInt += 100;
}
Here's the test code :-
void _tmain()
{
//Fill up Generation-0
DoLotsOfAllocs();
Test^ t1 = gcnew Test();
//Need a gap between the two Test objects to avoid
//the interior pointer being pinned too (the GC seems
//to pin an area and not just an object)
DoLotsOfAllocs(1000);
Test^ t2 = gcnew Test();
pin_ptr<int> p1 = &t1->m_i;
interior_ptr<int> p2 = &t2->m_i;
NativeFunc(p1);
Console::WriteLine(t1->m_i);
printf("pinned p1 = %p : interior ptr p2 = %p\r\n",p1,p2);
DoLotsOfAllocs(); //Force a Gen-0 GC
printf("pinned p1 = %p : interior ptr p2 = %p\r\n",p1,p2);
Here's the output I got (obviously, addresses won't match on other machines) :-
100
pinned p1 = 00ACA244 : interior ptr p2 = 00ACD130
pinned p1 = 00ACA244 : interior ptr p2 = 00AA8D54
p1
(the pinned pointer) retains its value even after a GC cycle, while p2
(an interior pointer) has changed confirming that while the unpinned object has been relocated, the pinned object has remained fixed in the CLI heap.
Passing to native code
If all we needed to do was directly access the underlying object via a pointer and do pointer operations (like pointer arithmetic and comparison), then it'd wholly suffice to purely use interior pointers. But interior pointers do not convert to native pointers and thus cannot be passed to native functions expecting native pointers. That's where pinned pointers come in handy with their implicit conversion to native pointers.
//Native function
#pragma unmanaged
void Test2(wchar_t* p)
{
wchar_t vowarr[] = L"aeiou";
while(*p++ = wcschr(vowarr,*p) ? towupper(*p) : *p);
}
//Mixed mode function
#pragma managed
void Test1()
{
String^ s = "hello world";
pin_ptr<Char> p = const_cast< interior_ptr<Char> >(PtrToStringChars(s));
Test2(p);
Console::WriteLine(s);
}
PtrToStringChars
returns a const
interior pointer which we first const_cast
to a volatile
interior pointer which gets implicitly converted to a pinned pointer. Now we pass this pinned pointer to Test2(...)
which accepts an unmanaged wchar_t*
and the pinned pointer is implicitly converted to the unmanaged pointer.
Now that I've shown you how pinned pointers have advantages and uses over interior pointers, I've got to tell you this - only use pinned pointers when you have to and even when you have to, make sure that your pinned pointer does not remain in scope for too long. This is explained in the next section.
Pinning pointers and heap fragmentation
Lets assume that we have 3 objects O1, O2 and O3 in the CLI heap [assume Gen-0], which will now look like this :-
O1 | O2 | O3 | Free Space |
Now, assume that O2 is now an unreachable object and a GC occurs. After GC the CLI heap will look like this :-
O1 | O3 | Free Space |
Now assume that O3 is a pinned object (means there are 1 or more pinning pointers pointing to it). If so, since the GC cannot relocate O3, the CLI heap will look like this :-
O1 | O3 | Free Space |
Now imagine what happens if we had numerous such pinned objects (indicated by a gray background); the CLI heap would look something like :-
O1 | O3 | O5 | O6 | O7 | O8 | Free Space |
Now, the heap is thoroughly fragmented, which makes GC cycles and memory allocation for new objects rather slower and less efficient than otherwise, and there is also the risk that there won't be a large enough contiguous block of free space to allocate a large object (say an array) which will result in some kind of memory exception.
Recommendations for using pinning pointers
[ Please note that these are strictly my ideas and could be remarkably erroneous for all I know, but with feedback from some of you out there who might know better, I hope to narrow down the level of inaccuracy. ]
-
Use pinning pointers only when absolutely necessary, else try and use interior pointers or tracking references.
-
When you do use pinning pointers, try and reduce the duration of their scope (the longer they remain in scope, the greater the risk of heap fragmentation).
-
Try to avoid pinning generation-0 objects (since GC cycles occur most frequently in generation-0).
-
If you need to pin multiple objects, try and allocate those objects together, so that the pinned objects will all be in one contiguous area in the CLI heap and the extent of fragmentation will be reduced.
-
During interop calls, first check and see if the marshalling layer does any pinning for you, and do your own pinning only if required.
-
Avoid situations where you pass a pinned pointer to a native pointer and risk using the native pointer after the pinned pointer has gone out of scope. Once the pinned pointer is out of scope, the object is no longer pinned and might get relocated or collected by the GC, in which case your native pointer is now pointing to a random area in the CLI heap.
Conclusion
I thoroughly enjoyed writing this article and am feeling rather proud of the fancy html-table graphics that I used for my CLI heap representations. If any of you feel that the graphics sucked, please don't let me know, so I can continue basking in an idiotic glory that makes sense only to me. On a more serious note, please feel free to submit you unmitigated feedback so that I can improve the article to the maximum extent possible. Thank you.
Copy from:
http://www.codeproject.com/Articles/8914/Guide-to-using-Pinning-Pointers