GC behavior when pinning an object
Asked Answered
A

1

37

While browsing through the code of PinnableObjectCache from mscorlib, I've encountered the following code:

for (int i = 0; i < m_restockSize; i++)
{
    // Make a new buffer.
    object newBuffer = m_factory();

    // Create space between the objects.  We do this because otherwise it forms 
    // a single plug (group of objects) and the GC pins the entire plug making 
    // them NOT move to Gen1 and Gen2. By putting space between them
    // we ensure that object get a chance to move independently (even if some are pinned).  
    var dummyObject = new object();
    m_NotGen2.Add(newBuffer);
}

It got me wondering what the reference to a plug means? While trying to pin an object in memory, wouldn't the GC pin the specific address specified for the object? What is this plug behavior actually doing and why is there a need to "space out" between the objects?

Amann answered 14/11, 2014 at 9:56 Comment(14)
There's a bit more info in the blog post blogs.msdn.com/b/maoni/archive/2005/10/03/…, scroll down to "Fragmentation control". Bit I'm not entirely sure if it answer the question?!Pyroclastic
@MattWarren He talks about Demotion where objects between spaced pinned objects dont get promoted. But in this example the author deliberately allocated a space between the pinned objects in order to make sure they get promoted independently. unfortunately it doesn't talk about the plug behavior :\Amann
Looks like a way to create padding in memory. Given dummyObject rather quickly, there should be some 'cleared' space after newBuffer (assuming the allocation is adjacent). Perhaps the minimum pinning space is twice IntPtr.Size?Dipterocarpaceous
@YuvalItzchakov actually Maoni is a she, see channel9.msdn.com/posts/…, but I get your point. But I was thinking that the code you posted is a way of preventing the behaviour in the blog. But I'm only guessing.Pyroclastic
@MattWaren My bad! Didn't see that.Amann
@Dipterocarpaceous Ill try testing that out.Amann
@YuvalItzchakov: I have no idea how you would test that ;p Please do tell if you find out more. Interesting stuff.Dipterocarpaceous
@Dipterocarpaceous I'm going to try and allocate a several pinned objects with spaces between them and without and try to determine the GC behavior between generations.Amann
dummyObject 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
@Fledgling assuming it was allocated, do you have any idea why they'd do that?Amann
That comment was written by a newbie Microsoft programmer that had only half a clue. It is partly accurate, the problem he was asked to find a workaround for is a real one. And yes, plugs vs gaps do exist in the GC compacting algorithm and it is something to fret about when you allocate several buffers that are likely to be pinned. The ones wedged in between that might be unpinned when the GC runs won't be moved. The rest of comment, no, not really. It is hacky code, having it appear twice loses a thousand elegant points, it is pretty harmless and does try to address the core problem.Ninetieth
@HansPassant Could you please elaborate more on the GC behavior perhaps in an answer?Amann
If I understand the above code correctly, then it implies that the GC may accidentally pin objects that aren't set to be pinned if they are wedged between pinned objects. What that means is that the wedged objects won't be able to move up to different GC Gens as their lifetime grows. I don't think this is true. Maoni's slides clearly show just pinned objects have this effect of not moving up in Gens, and makes no notion of accidental pinning of these "plugs". I think it's just a forgotten line of code to be completely honest with you. Let us know if you find some way to test this out.Springer
I think this may also have something to do with the large object heap, and tricks with page tables. You may find the “newBuffer” is sized to take up a number of complete pages.Alagez
A
19

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.

Amann answered 23/11, 2014 at 17:24 Comment(6)
Sorry if this is a daft question, but what's obj1? Should it be byteArr1?Titbit
@Titbit You're completely right. I had a couple of iterations with the code and I've forgotten to rename the variables in the answer.Amann
You should add something after the call to GC to keep the object alive, as I think it is just the debugger that are keeping them alive.Alagez
@Ian I'm not sure I follow. The point of the question was to see what happens during GC collections when some objects are pinned while others weren't, regardless if the unpinned objects were alive or not.Amann
What if byteArr3 and byteArr4 are garbage collected before the call to GCHandle.Alloc(byteArr5, GCHandleType.Pinned);? (I just don't experiments that are not 100% repeatable....)Alagez
@IanRingrose You see from the dumpheap command that they weren't collected. The purpose of this test was to check what happens to the pinned objects when an allocation occurred.Amann

© 2022 - 2024 — McMap. All rights reserved.