What assumptions are made about/by the Garbage Collector at Runtime?

346 views
Skip to first unread message

Kyle Stanly

unread,
Jun 29, 2016, 11:35:39 AM6/29/16
to golang-dev
I've a feeling a lot of potential assumptions are made at runtime, and I'm trying to keep track of them myself, but I've a few questions.

Runtime Allocations

1) Is there a hard limit on how much the runtime is allowed to allocate fromt he GC? If there is, what happens when this limit is hit?

2) Is the Runtime address space separate from the User-space address space? What I mean is, can the runtime get an address that is recycled from a user's previous allocation, and vice-verse?

3) Is it possible for the GC to reclaim memory allocated in runtime/malloc.go? I feel I'm having an issue wherein data is being collected even if it is collectable (I.E, if I log the move of objects from one place to another, as in transfer ownership, sometimes they disappear entirely. Logs show them being moved [I keep track of both address and contents of the object] but never again after being moved, and it is inconsistent, as not all moved objected disappear...).

4) Is there a way to determine precisely when runtime data is collected? (Also is there a way to properly log the runtime? Println statements can only do so much)

5) Is there a reason why memory cannot escape the heap during escape analysis? How precisely does the "noescape" function prevent it from escaping? Does the '//go:noescape' annotation rely on this function as well?

Garbage Collection

1) Are Runtime allocations treated differently from user allocations?


I'd appreciate it if these questions were answered, or if I was redirected to a source that could better do so, even if it's source code. I'm trying to keep the questions general rather than specific to my problem, as if I kept it specific it'd be much more time-consuming.

Kyle Stanly

unread,
Jun 29, 2016, 11:39:11 AM6/29/16
to golang-dev
Runtime Allocations.3 has a typo. I meant "I feel I'm having an issue wherein data is being collected even if it is reachable"

Ian Lance Taylor

unread,
Jun 29, 2016, 11:48:08 AM6/29/16
to Kyle Stanly, golang-dev
On Wed, Jun 29, 2016 at 8:35 AM, Kyle Stanly <thei...@gmail.com> wrote:
>
> Runtime Allocations
>
> 1) Is there a hard limit on how much the runtime is allowed to allocate
> fromt he GC? If there is, what happens when this limit is hit?

Do you mean the runtime rather than the rest of the program? No,
there is no such limit.


> 2) Is the Runtime address space separate from the User-space address space?
> What I mean is, can the runtime get an address that is recycled from a
> user's previous allocation, and vice-verse?

In general the address spaces are shared. The runtime does allocate
some memory separately from the GC, but it's a relatively small
amount. Look for the fields of type fixalloc in the mheap type.


> 3) Is it possible for the GC to reclaim memory allocated in
> runtime/malloc.go? I feel I'm having an issue wherein data is being
> collected even if it is collectable (I.E, if I log the move of objects from
> one place to another, as in transfer ownership, sometimes they disappear
> entirely. Logs show them being moved [I keep track of both address and
> contents of the object] but never again after being moved, and it is
> inconsistent, as not all moved objected disappear...).

Yes, the GC reclaims memory allocated by the mallocgc function. All
user program allocations and most runtime allocations go through that
function.

If you think that reachable memory is being collected the obvious
first thing to check is the type. The garbage collector is sensitive
to types; storing a pointer value as a uintptr type will break it.

> 4) Is there a way to determine precisely when runtime data is collected?
> (Also is there a way to properly log the runtime? Println statements can
> only do so much)

See the GODEBUG variable. It can print a trace whenever the garbage
collector runs. But that won't help identify when specific memory
blocks are collected.

> 5) Is there a reason why memory cannot escape the heap during escape
> analysis? How precisely does the "noescape" function prevent it from
> escaping? Does the '//go:noescape' annotation rely on this function as well?

I don't really understand the question. Either I misunderstand it or
you misunderstand what escape analysis does. The //go:noescape
comment tells the compiler that pointers passed to a function do not
escape. This is used for functions written in assembler.


> Garbage Collection
>
> 1) Are Runtime allocations treated differently from user allocations?

No.

Ian

Josh Bleecher Snyder

unread,
Jun 29, 2016, 11:50:47 AM6/29/16
to Kyle Stanly, golang-dev
Running with envvar GODEBUG=gctrace=1 will tell you everything that gets allocated and freed, with stack traces. It's a lot of data, but it can be very helpful in tracking down exactly what is happening.

There are other GODEBUG settings as well. Check out the package runtime docs.

Josh
--
You received this message because you are subscribed to the Google Groups "golang-dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-dev+...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Austin Clements

unread,
Jun 29, 2016, 11:54:18 AM6/29/16
to Josh Bleecher Snyder, Kyle Stanly, golang-dev
GODEBUG=gctrace=1 just reports high level stats about each GC. I assume Josh meant GODEBUG=allocfreetrace=1.

Austin Clements

unread,
Jun 29, 2016, 11:56:55 AM6/29/16
to Ian Lance Taylor, Kyle Stanly, golang-dev
On Wed, Jun 29, 2016 at 11:48 AM, Ian Lance Taylor <ia...@golang.org> wrote:
On Wed, Jun 29, 2016 at 8:35 AM, Kyle Stanly <thei...@gmail.com> wrote:
> 3) Is it possible for the GC to reclaim memory allocated in
> runtime/malloc.go? I feel I'm having an issue wherein data is being
> collected even if it is collectable (I.E, if I log the move of objects from
> one place to another, as in transfer ownership, sometimes they disappear
> entirely. Logs show them being moved [I keep track of both address and
> contents of the object] but never again after being moved, and it is
> inconsistent, as not all moved objected disappear...).

Yes, the GC reclaims memory allocated by the mallocgc function.  All
user program allocations and most runtime allocations go through that
function.

If you think that reachable memory is being collected the obvious
first thing to check is the type.  The garbage collector is sensitive
to types; storing a pointer value as a uintptr type will break it.

Another possibility is that you're missing write barriers. Normal Go assignments will generate appropriate write barriers when you copy pointers, but if you call things like memmove or pass the pointer through a uintptr, the garbage collector can lose track of it.

Kyle Stanly

unread,
Jun 29, 2016, 12:18:21 PM6/29/16
to golang-dev, thei...@gmail.com
5) Is there a reason why memory cannot escape the heap during escape 
analysis? How precisely does the "noescape" function prevent it from 
escaping? Does the '//go:noescape' annotation rely on this function as well? 

I don't really understand the question.  Either I misunderstand it or 
you misunderstand what escape analysis does.  The //go:noescape 
comment tells the compiler that pointers passed to a function do not 
escape.  This is used for functions written in assembler. 

Apologies, I'm making a lot of typos lately. I meant "why can't memory escape to the heap during the runtime", but it got jumbled in with "due to escape analysis". For example, if I try to allocate something by allocating it on the stack, and then explicitly return a pointer to that, the escape analysis would cause it to be allocated on the heap (or so I think). 

If you think that reachable memory is being collected the obvious 
first thing to check is the type.  The garbage collector is sensitive 
to types; storing a pointer value as a uintptr type will break it. 

This is a rather crucial piece of information I may be looking over. For some reason the types created in reflect.go (the ones I created explicitly... I guess I'll have to create another CL, but it'd be rather long overall) have the wrong size. I.E, it thinks the size of a particular linked node (custom) is 32 bytes when it is 48 bytes (I use fixed types I know the size of), so I explicitly do the math myself.

The thing is, I used the same compile-time tricks runtime/hashmap.go does with it's 'bmap' structure, the structure only contains it's concrete variables, but the compile-type maintains the actual size and structure. To give an example

type bucketChain struct {
    next *bucketChain
    flags uintptr
    /*
        Two extra fields can be accessed through the use of unsafe.Pointer, needed because we take
        each key and value by value, (if it is a indirectKey, then we just store the pointer) and can
        vary in size. Use cdataOffset to obtain key, and cdataOffset + t.keysize to obtain value
    */
}

In the compiler it is encoded as such...

type bucketChain struct {
    next *bucketChain
    flags uintptr
    key keyType // or *keyType
    value valType // or *valType
}

However, unfortunately, the above is only enough for two pointers (as next and flags are pointer-aligned, so they're 16 bytes on my system; if keyType and valType were pointer types, they'd be another 16, hence 32 bytes in total). The funny thing is that the compiler correctly acknowledges keyType and valType as 16-bytes... but this is probably getting confusing without code to look at as I could have done something else wrong instead.

I'm trying to solve this problem on my own however so I don't need to rely on asking questions, but if the type is wrong, it's possible that the GC would interpret it differently than from how I expect.

Austin Clements

unread,
Jun 29, 2016, 12:25:00 PM6/29/16
to Kyle Stanly, golang-dev
On Wed, Jun 29, 2016 at 12:18 PM, Kyle Stanly <thei...@gmail.com> wrote:
5) Is there a reason why memory cannot escape the heap during escape 
analysis? How precisely does the "noescape" function prevent it from 
escaping? Does the '//go:noescape' annotation rely on this function as well? 

I don't really understand the question.  Either I misunderstand it or 
you misunderstand what escape analysis does.  The //go:noescape 
comment tells the compiler that pointers passed to a function do not 
escape.  This is used for functions written in assembler. 

Apologies, I'm making a lot of typos lately. I meant "why can't memory escape to the heap during the runtime", but it got jumbled in with "due to escape analysis". For example, if I try to allocate something by allocating it on the stack, and then explicitly return a pointer to that, the escape analysis would cause it to be allocated on the heap (or so I think). 

Runtime code is subject to the same escape analysis as any other code, which means that just like other Go code you don't really have control over whether something goes on the stack or the heap. *However*, there is a rule in the runtme that if escape analysis determines that a local variable needs to be moved to the heap because it escapes, the compile will fail. In the runtime, if you want to allow something to be heap allocated, you need to use new or make explicitly.


If you think that reachable memory is being collected the obvious 
first thing to check is the type.  The garbage collector is sensitive 
to types; storing a pointer value as a uintptr type will break it. 

This is a rather crucial piece of information I may be looking over. For some reason the types created in reflect.go (the ones I created explicitly... I guess I'll have to create another CL, but it'd be rather long overall) have the wrong size. I.E, it thinks the size of a particular linked node (custom) is 32 bytes when it is 48 bytes (I use fixed types I know the size of), so I explicitly do the math myself.

The thing is, I used the same compile-time tricks runtime/hashmap.go does with it's 'bmap' structure, the structure only contains it's concrete variables, but the compile-type maintains the actual size and structure. To give an example

type bucketChain struct {
    next *bucketChain
    flags uintptr
    /*
        Two extra fields can be accessed through the use of unsafe.Pointer, needed because we take
        each key and value by value, (if it is a indirectKey, then we just store the pointer) and can
        vary in size. Use cdataOffset to obtain key, and cdataOffset + t.keysize to obtain value
    */
}

In the compiler it is encoded as such...

type bucketChain struct {
    next *bucketChain
    flags uintptr
    key keyType // or *keyType
    value valType // or *valType
}

However, unfortunately, the above is only enough for two pointers (as next and flags are pointer-aligned, so they're 16 bytes on my system; if keyType and valType were pointer types, they'd be another 16, hence 32 bytes in total). The funny thing is that the compiler correctly acknowledges keyType and valType as 16-bytes... but this is probably getting confusing without code to look at as I could have done something else wrong instead.

I'm trying to solve this problem on my own however so I don't need to rely on asking questions, but if the type is wrong, it's possible that the GC would interpret it differently than from how I expect.

The garbage collector cares about the "size" and "ptrdata" fields of the type, and of course its pointer bitmap in "gcdata". If any of those are wrong, it could fail to trace pointers.

Kyle Stanly

unread,
Jun 29, 2016, 12:25:06 PM6/29/16
to golang-dev, ia...@golang.org, thei...@gmail.com
I always explicitly use typedmemmove for the user's data, but for my own I thought simply moving them from one place to another would actually work... if you're referring to the GC's write barriers, then it makes sense that a mutator would not be able to gray out the new reference if the write barrier wasn't executed prior to the write and was left white (but wouldn't this also violate the invariant of now black object pointing to a white object?). Lets say we have the following structure...

type node struct { next *node }

and

type root struct { n *node }

If I had root's 'n' point to a node, and then swap it's 'n' with another root, then would theoretically, when the mutator exchanges nodes, and a write barrier is not executed, then both appear as being white to the GC, leading to both being collected?

r1 -> n1
r2
-> n2


// swap


r1
-> n2
r2
-> n1

Without the write barrier, would this spell potential disaster?

If we moved the 

Kyle Stanly

unread,
Jun 29, 2016, 12:37:16 PM6/29/16
to golang-dev, thei...@gmail.com
The garbage collector cares about the "size" and "ptrdata" fields of the type, and of course its pointer bitmap in "gcdata". If any of those are wrong, it could fail to trace pointers.

I suppose it was my fault for trying to step-over the issue by just explicitly using mallocgc to allocate enough. Even if the GC wasn't collecting my node early, the data itself could have been, since it wouldn't scan that far after it... Would it even be able to reclaim the memory allocated after the "size"?

For example: At this point I was allocating like such...

b := mallocgc(16 + t.keysize + t.valuesize, t.nodetype, true)

Would it only scan and reclaim data up to t.nodetype.size and leak the data allocated after?

Kyle Stanly

unread,
Jun 29, 2016, 1:24:54 PM6/29/16
to golang-dev, ia...@golang.org, thei...@gmail.com
Wait maybe I read this wrong. Normal Go assignments are treated the same way in both runtime and compile-time, right? If that is the case, then most likely the issue is with the type itself.

Matthew Dempsky

unread,
Jun 29, 2016, 1:36:33 PM6/29/16
to Kyle Stanly, golang-dev
On Wed, Jun 29, 2016 at 9:37 AM, Kyle Stanly <thei...@gmail.com> wrote: 
For example: At this point I was allocating like such...

b := mallocgc(16 + t.keysize + t.valuesize, t.nodetype, true)

Would it only scan and reclaim data up to t.nodetype.size and leak the data allocated after?

mallocgc(size, typ, b) means to allocate size bytes of memory, and to treat it as an array of size/typ.size elements.  mallocgc assumes size is a multiple of typ.size.

Austin Clements

unread,
Jun 29, 2016, 1:39:02 PM6/29/16
to Kyle Stanly, golang-dev
On Wed, Jun 29, 2016 at 12:37 PM, Kyle Stanly <thei...@gmail.com> wrote:
The garbage collector cares about the "size" and "ptrdata" fields of the type, and of course its pointer bitmap in "gcdata". If any of those are wrong, it could fail to trace pointers.

I suppose it was my fault for trying to step-over the issue by just explicitly using mallocgc to allocate enough. Even if the GC wasn't collecting my node early, the data itself could have been, since it wouldn't scan that far after it... Would it even be able to reclaim the memory allocated after the "size"?

For example: At this point I was allocating like such...

b := mallocgc(16 + t.keysize + t.valuesize, t.nodetype, true)

Would it only scan and reclaim data up to t.nodetype.size and leak the data allocated after?

Actually, the effect of this may be even stranger. mallocgc assumes that if size (the first argument) is bigger than t.nodetype.size, that you're allocating an array of t.nodetype. It will repeat t.nodetype's pointer bitmap up to the last whole multiple of t.nodetype.size, and anything past there will be considered non-pointer data.

Austin Clements

unread,
Jun 29, 2016, 1:40:14 PM6/29/16
to Kyle Stanly, golang-dev, Ian Lance Taylor
Yes, normal Go assignments will have the appropriate GC write barriers if what you're assigning is a pointer or contains pointers.

--

Kyle Stanly

unread,
Jun 29, 2016, 4:52:16 PM6/29/16
to golang-dev, thei...@gmail.com
Okay, so apparently, after a lot of inserting println statements...

It seems that the t.nodetype.size remains consistent, regardless of the data. I.E, it remains 32 bytes even if I increase the size of the key and value to 32 bytes. However, in the compiler (I add a printf to print out the size of the key and value, and then overall size) it shows it's correct size. Hence it clearly is not being set correctly, and I have to ask... where precisely might t.nodetype.size be set to it's actual Width?

I know in cmd/compile/internal/gc/reflect.go, you must create your own custom types (like mapbucket and hmap), and have them encoded in dtypesym, but where else? To emphasize, I only need to find out where the actual size is set to the Width.

Kyle Stanly

unread,
Jun 29, 2016, 5:31:07 PM6/29/16
to golang-dev, thei...@gmail.com
Okay so I saw my issue. Apparently I was lazy and in cmd/compile/internal/gc/fmt.go, typefmt(...) function, I was lazy and did

if nodetype == t || nodetype2 == t || nodetype3 ==3 || ... { return "nodetype" }

I figured it was for debugging information, not to map each string to it's type. After giving each nodetype it's own string, it gives me the appropriate width.
Reply all
Reply to author
Forward
0 new messages