Ok, so after several attempts to get official replies from people with "inside knowledge", I decided to experiment a little myself.
What I tried to do is re-produce the scenario where I have a couple of pinned objects and some unpinned objects between them (i used a byte[]
) to try and create the effect where the unpinned objects don't move the a higher generation inside the GC heap.
The code ran on my Intel Core i5 laptop, inside a 32bit console application running Visual Studio 2015 both in Debug and Release. I debugged the code live using WinDBG.
The code is rather simple:
private static void Main(string[] args)
{
byte[] byteArr1 = new byte[4096];
GCHandle obj1Handle = GCHandle.Alloc(byteArr1 , GCHandleType.Pinned);
object byteArr2 = new byte[4096];
GCHandle obj2Handle = GCHandle.Alloc(byteArr2, GCHandleType.Pinned);
object byteArr3 = new byte[4096];
object byteArr4 = new byte[4096];
object byteArr5 = new byte[4096];
GCHandle obj4Handle = GCHandle.Alloc(byteArr5, GCHandleType.Pinned);
GC.Collect(2, GCCollectionMode.Forced);
}
I started out with taking a look at the GC heap address space using !eeheap -gc
:
generation 0 starts at 0x02541018
generation 1 starts at 0x0254100c
generation 2 starts at 0x02541000
ephemeral segment allocation context: none
segment begin allocated size
02540000 02541000 02545ff4 0x4ff4(20468)
Now, I step through the code running and watch as the objects get allocated:
0:000> !dumpheap -type System.Byte[]
Address MT Size
025424e8 72101860 4108
025434f4 72101860 4108
02544500 72101860 4108
0254550c 72101860 4108
02546518 72101860 4108
Looking at the addresses I can see they're all currently at generation 0 as it starts at 0x02541018
. I also see that the objects are pinned using !gchandles
:
Handle Type Object Size Data Type
002913e4 Pinned 025434f4 4108 System.Byte[]
002913e8 Pinned 025424e8 4108 System.Byte[]
Now, I step through the code untill i get to the line which runs GC.Collect
:
0:000> p
eax=002913e1 ebx=0020ee54 ecx=00000002 edx=00000001 esi=025424d8 edi=0020eda0
eip=0062055e esp=0020ed6c ebp=0020edb8 iopl=0 nv up ei pl nz na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000206
0062055e e80d851272 call mscorlib_ni+0xa28a70 (GC.Collect) (72748a70)
And now, anticipating what happens, i check the GC generation address again using !eeheap -gc
and i see the following:
Number of GC Heaps: 1
generation 0 starts at 0x02547524
generation 1 starts at 0x0254100c
generation 2 starts at 0x02541000
The starting address for generation 0 has been moved from 0x02541018 to 0x02547524.
Now, i check the address of the pinned and none pinned byte[]
objects:
0:000> !dumpheap -type System.Byte[]
Address MT Size
025424e8 72101860 4108
025434f4 72101860 4108
02544500 72101860 4108
0254550c 72101860 4108
02546518 72101860 4108
And I see they have all stayed at the same address. But, the fact that generation 0 now starts at 0x02547524 means they've all been promoted to generation 1.
Then, I remember reading something about that behavior in the book Pro .NET Performance, it states the following:
Pinning an object prevents it from being moved by the garbage
collector. In the generational model, it prevents promotion of pinned
objects between generations. This is especially significant in the
younger generations, such as generation 0, because the size of
generation 0 is very small. Pinned objects that cause fragmentation
within generation 0 have the potential of causing more harm than it
might appear from examining pinned before we introduced generations
into the the picture. Fortunately, the CLR has the ability to promote
pinned objects using the following trick: if generation 0 becomes
severely fragmented with pinned objects, the CLR can declare the
entire space of generation 0 to be considered a higher generation and
allocate new objects from a new region of memory that will become
generation 0. This is achieved by changing the ephemeral segment.
And this actually explains the behavior i'm seeing inside WinDBG.
So, to conclude and until anyone has any other explanation, I think the comment isn't correct and doesn't really capture what is really happening inside the GC. If anyone has anything to elaborate, I'd be glad to add.
dummyObject
rather quickly, there should be some 'cleared' space afternewBuffer
(assuming the allocation is adjacent). Perhaps the minimum pinning space is twiceIntPtr.Size
? – DipterocarpaceousdummyObject
is almost certainly going to not actually be created if optimizations are on, as the compiler can prove its never used, so the code likely isn't doing what he thinks its doing anyway. – Fledgling