17

In C#, ref and out params are, as far as I know, passed by passing only the raw address of the relevant value. That address may be an interior pointer to an element in an array or a field within an object.

If a garbage collection occurs, it's possible that the only reference to some object is through one of these interior pointers, as in:

using System;

public class Foo
{
    public int field;

    public static void Increment(ref int x) {
        System.GC.Collect();
        x = x + 1;
        Console.WriteLine(x);
    }

    public static void Main()
    {
        Increment(ref new Foo().field);
    }
}

In that case, the GC needs to find the beginning of the object and treat the entire object as reachable. How does it do that? Does it have to scan the entire heap looking for the object that contains that pointer? That seems slow.

munificent
  • 11,946
  • 2
  • 38
  • 55
  • I maybe wrong but Doesn't the garbage collector hash all references for any created object for quick look up – johnny 5 Nov 21 '17 at 17:38
  • @johnny5: So what? It still can't tell that the `new Foo()` is referenced by anything. – SLaks Nov 21 '17 at 17:39
  • `new Foo()` is on the stack here. This is the same as `var f = new Foo(); Increment(ref f.field);`. – Blorgbeard Nov 21 '17 at 17:40
  • 1
    @Blorgbeard: No; there, `f` is stored in a local variable, which creates a GC root. Here, it isn't. – SLaks Nov 21 '17 at 17:41
  • Well, it'll know a new `Foo` has been created, and as soon as the call to `Increment` gets made it'll increment its reference count. `GC.Collect()` will then ignore that object as it has a reference count of 1, then after that call it'll decrement that count so the *next* time GC happens it'll dispose of it... This example doesn't look like it should be hard for the GC to track it at all. – GPW Nov 21 '17 at 17:45
  • Does the GC require a local variable? `newobj` drops a reference on the stack. I was under the impression that would be enough for the GC. – Blorgbeard Nov 21 '17 at 17:53
  • This not an interior pointer, just a raw pointer at runtime that the GC doesn't track. The GC finds the object reference back when it walks the stack. – Hans Passant Nov 21 '17 at 18:27
  • Hans, the problem is the object is no longer on the stack, only an interior pointer to its field is. Note the IL in the answer by SLaks below. ldflda pops the object off the stack and pushes the address of the field. – munificent Nov 21 '17 at 23:40
  • No real idea why you think so, it will most definitely find Main() back when walking the stack. Whether or not Increment() gets inlined doesn't matter. No variable is required to help the GC find the object reference, a primary duty of the jitter is to build a table that tells the GC where those references are stored. Most of the time they are stored in a CPU register. – Hans Passant Nov 22 '17 at 00:21
  • 1
    Finding Main() doesn't help. Main's stack has no reference to an instance of Foo. It's never stored in a local variable, and the temporary is popped off the stack before Increment() is called. The *only* thing on the stack, as far as I can tell, is the address of the field within Foo. – munificent Nov 22 '17 at 01:09
  • Seems like the site-internal searching function at *StackOverflow* is not so great--how did it not find *this* question before I went to the trouble of posting a [basically identical version](https://stackoverflow.com/questions/48287071) of my own. The similarities are quite eerie, down to voicing nearly the exact same worry: *Does it have to scan the entire heap looking for the object that contains that pointer? That seems slow.* vs. *Are there any situations where the CLR has to recover a containing object given only a managed pointer, and if so, how would this be done and is it efficient?* – Glenn Slayden Jan 16 '18 at 22:12

3 Answers3

6

The garbage collector will have a fast way to find the start of an object from a managed interior pointer. From there it can obviously mark the object as "referenced" when doing the sweeping phase.

Don't have the code for the Microsoft collector but they would use something similar to Go's span table which has a fast lookup for different "spans" of memory which you can key on the most significant X bits of the pointer depending on how large you choose the spans to be. From there they use the fact that each span contains X number of objets of the same size to very quickly find the header of the one you have. It's pretty much an O(1) operation. Obviously the Microsoft heap will be different since it's allocated sequentially without regard for object size but they will have some sort of O(1) lookup structure.

https://github.com/puppeh/gcc-6502/blob/master/libgo/runtime/mgc0.c

// Otherwise consult span table to find beginning.
// (Manually inlined copy of MHeap_LookupMaybe.)
k = (uintptr)obj>>PageShift;
x = k;
x -= (uintptr)runtime_mheap.arena_start>>PageShift;
s = runtime_mheap.spans[x];
if(s == nil || k < s->start || (const byte*)obj >= s->limit || s->state != MSpanInUse)
    return false;
p = (byte*)((uintptr)s->start<<PageShift);
if(s->sizeclass == 0) {
    obj = p;
} else {
    uintptr size = s->elemsize;
    int32 i = ((const byte*)obj - p)/size;
    obj = p+i*size;
}

Note that the .NET garbage collector is a copying collector so managed/interior pointers need to be updated whenever the object is moved during a garbage collection cycle. The GC will be aware of where in the stack interior pointers are for each stack frame based on the method parameters known at JIT time.

tumtumtum
  • 1,052
  • 9
  • 11
  • 2
    Ah, yes. That makes sense. Digging around, I'm guessing the corresponding code in dotnetcore is gc_heap::find_object() at line 17143 in https://github.com/dotnet/coreclr/blob/master/src/gc/gc.cpp. Thanks! – munificent Nov 28 '17 at 00:50
3

Your code compiles to

    IL_0001: newobj instance void Foo::.ctor()
    IL_0006: ldflda int32 Foo::'field'
    IL_000b: call void Foo::Increment(int32&)

AFAIK, the ldflda instruction creates a reference to the object containing the field, for as long as the address is on the stack (until the call completes).

SLaks
  • 868,454
  • 176
  • 1,908
  • 1,964
  • 1
    My understanding is that ldflda pushes the address of the field itself (i.e. an interior pointer) and not the object containing the field. So the ldflda pops the reference to the object and pushes back the address of the field, which means the object itself is no longer on the stack. – munificent Nov 21 '17 at 23:34
  • @munificent: I think that it pushes the object itself as a GC root (not on the stack). I'm not sure, though. – SLaks Nov 22 '17 at 01:27
  • ldflda pops the object itself off the stack. it wouldn't be able to "push" the object as GC root because when would it know to pop it off? The only sensible way to know would be using the stack.....I'm pretty sure the object is kept alive by the interior pointer. – tumtumtum Nov 28 '17 at 17:50
1

The garbage collector works in three basic steps:

  1. Mark all objects that are still alive.
  2. Collect the objects that are not marked as alive.
  3. Compact the memory.

Your concern is step 1: How does the GC figure out that it shouldn't collect objects behind ref and out params?

When the GC performs a collection, it starts with a state where none of the objects is considered alive. It then goes from the root references and marks all those objects as alive. Root references are all references on the stack and in static fields. Then the GC goes recursively into the marked objects and marks all objects as alive that are referenced from them. This is repeated until no objects are found that are not already marked as alive. The result of this operation is an object graph.

A ref or out parameter has a reference on the stack, and so the GC will mark the respective object as alive, because the stack is a root for the object graph.

At the end of the process, the objects with only internal references are not marked, because there is no path from the root references that would reach them. This takes care of all circular references, too. These objects are considered dead and will be collected in the next step (that includes calling the finalizer, even though there is no guarantee for that).

At the end, the GC will move all alive objects to a continuous area of memory at the beginning of the heap. The rest of the memory will filled with zeroes. That simplifies the process of creating new objects, since their memory can always be allocated at the end of the heap and all fields already have the default values.

It is true that the GC needs some time to do all of this, but it still does it reasonably fast, due to some optimizations. One of the optimizations is to separate the heap into generations. All newly allocated objects are generation 0. All objects surviving the first collection are generation 1 and so forth. Higher generations are only collected if collecting lower generations does not free up enough memory. So, no, the GC does not always have to scan the entire heap.

You have to consider that, while the collection takes some time, allocating new objects (which happens much more often than a garbage collection) is much faster than in other implementations, where the heap looks more like a swiss cheese and you need some time to find a hole big enough for the new object (which you still need to initialize).

Sefe
  • 13,731
  • 5
  • 42
  • 55
  • 4
    Yes, I understand how a GC works in general. You note: "A ref or out parameter has a reference on the stack, and so the GC will mark the respective object as alive, because the stack is a root for the object graph." With a ref/out param, the stack only has the address of the field within the object (an interior pointer), not the object itself. Given that interior pointer, how does it find the beginning of the object? – munificent Nov 21 '17 at 23:35
  • @munificent:Why do you assume there is no reference to the object? This is a field, which is on an object. So there is always an object reference stored, too. – Sefe Nov 22 '17 at 09:05
  • 2
    "…there is always an object reference stored, too…"—no, this is the whole point of @munificent 's question. Using `ref` inherently creates runtime scenarios for which the GC cannot forsee, predict, or ***proactively*** track the containing object handle. To propagate (or "store" as you suggest) a relevant object handle alongside every use of `ref` (and there may not be one, *e.g.*, `ref` to value-type on the stack) at runtime would likely defeat the purpose of using `ref` in the first place. Rather, GC pains to reconstruct these only at GC-time. See https://stackoverflow.com/a/52829684/147511 – Glenn Slayden Apr 21 '19 at 22:29
  • "objects with only internal references are not marked... these objects are considered dead and will be collected" Er... the question is about "interior pointers" ([managed pointers](http://tooslowexception.com/managed-pointers-in-net/)) and if that's what you meant, you are incorrect. Interior pointers to an object are not weak references. They will prevent GC of the entire object. – Qwertie Apr 08 '20 at 18:31