Skip to content

runtime: large hashmaps confuse the garbage collector when GOMAXPROCS>=2 #6119

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
cookieo9 opened this issue Aug 12, 2013 · 17 comments
Closed
Milestone

Comments

@cookieo9
Copy link
Contributor

As discussed in https://groups.google.com/forum/#!topic/golang-nuts/aa14gSRKPM0 there
seems to be a memory leak caused by having a large map when running with
GOMAXPROCS>=2.

In http://play.golang.org/p/IhCBf2f_hT, will work properly (sit a a constant memory use
level) if GOMAXPROCS=1. But when GOMAXPROCS>=2, none of the slices allocated in the
loop get collected, and the memory use of the program keeps growing.

Attached are the outputs of the program when run with GCDEBUG=gctrace=1 under
GOMAXPROCS=1 and GOMAXPROCS=2

Not that changing the "knobs" at the top of the code can fix the situation:
 - If N_ELEMS=1e6 (instead of 1e7), then the problem doesn't happen
 - If the type of m_type is the commented out slice type instead of a map, then the problem doesn't happen either, even when growing the size of N_ELEMS to use a similar amount of RAM.

Which suggests that the issue only occurs with a sufficiently large enough hashmap.

go version devel +7064d3304d65 Mon Aug 12 22:04:10 2013 +0400 darwin/amd64

It has also be shown to be present on go version go1.1.1 darwin/amd64, and the original
issue in the thread was for linux.

Attachments:

  1. procs1.txt (1562 bytes)
  2. procs2.txt (692 bytes)
@robpike
Copy link
Contributor

robpike commented Aug 13, 2013

Comment 1:

Labels changed: added priority-soon, go1.2, removed priority-triage, go1.2maybe.

Owner changed to @randall77.

Status changed to Accepted.

@randall77
Copy link
Contributor

Comment 2:

Some initial data.  I instrumented the runtime to print out the big MSpans after each gc.
With GOMAXPROCS=1, there are only 3: one of 73728 pages (the main bucket table for the
map) and two with 19532 pages (the size of the big slice).  (I don't know why there are
2 of the latter - shouldn't there be only one?)
With GOMAXPROCS=2, there are more.  There's still one 73728 one, but the number of 19532
ones keeps growing.  I get 3-4, then 7ish, then probably something bigger because it
crashes.
I don't yet understand why maps, or why GOMAXPROCS matters.

@randall77
Copy link
Contributor

Comment 3:

I think I see the problem.
To iterate through the map, the compiler allocates a hash_iter object on the stack. 
This object contains, among other things, a pointer to the big main bucket table.  When
the stack is scanned, this pointer is a root that keeps the bucket table live.
On one processor this isn't a problem, because the hash table header object gets scanned
first and as a side effect marks the main bucket table as scanned.  On a multiprocessor,
however, while one thread is still busy with the hash table header (marking overflow
buckets and the like), the other thread gets this reference to the main bucket table and
seeing it hasn't been marked yet, proceeds to conservatively scan the whole thing.  It's
got lots of things in it that look like pointers, and these end up keeping the big slice
arrays alive.
The fix is to prevent GC from following pointers directly into the internals of the hash
map.  All GC access should be done through the Hmap via gc_iter/gc_next.  One way is to
make the pointer in the the hash_iter structure a uintptr instead of a byte*.  That
should hide it from the GC, at least when we have precise stack scanning on.  Probably
better is to have the hashtable implementation allocate all of its internals as
FlagNoPointers so that even if the GC did get a hold of an internal pointer, it won't
scan any internals.

Status changed to Started.

@alberts
Copy link
Contributor

alberts commented Aug 13, 2013

Comment 4:

I think we're hitting this issue on go1.1.1 from time to time. Once you've figured it
out, we'd appreciate some guidance on working around/avoiding this, or a fix in go1.1.3.

@randall77
Copy link
Contributor

Comment 5:

Try patching in http://golang.org/cl/12840043 and see if that helps.

@dvyukov
Copy link
Member

dvyukov commented Aug 13, 2013

Comment 6:

---------- Forwarded message ----------
From: Dmitry Vyukov <dvyukov@google.com>
Date: Mon, Aug 12, 2013 at 9:16 PM
Subject: GC vs hashmaps
To: Carl Shapiro <cshapiro@google.com>, Keith Randall <khr@google.com>
Cc: Carlos Castillo <cookieo9@gmail.com>, golang-nuts
<golang-nuts@googlegroups.com>, Jakob Borg <jakob@nym.se>
On Mon, Aug 12, 2013 at 6:06 PM, Jakob Borg <jakob@nym.se> wrote:
Right, indeed. It seems the map access is necessary to cause the
issue; I slightly reduced the problem program to
http://play.golang.org/p/Tkdqs0lWoc
Removing the "_ = m[0]" removes the issue.
Here is what happens here:
_ = m[0]
is turned into runtime.mapaccess1_fast64 call which returns a pointer into the huge
hashmap buffer. This pointer is left on stack in 0x18(rsp).
p := make([]int, N_ELEMS)
is turned into makeslice1 call. 0x18(rsp) refers to makeslice1 return value, which is a
pointer but not initialized until function returns. GC is triggered inside of makeslice1
and it scans 0x18(rsp) completely conservatively.
As the result 160MB hashmap buffer with all the hashes is scanned conservatively.
Questions:
1. We are going to zero function return values and local in C code as well, right?
2. We will need to do the same for assembly functions manually, right?

@dvyukov
Copy link
Member

dvyukov commented Aug 13, 2013

Comment 7:

_ = m[0]
is turned into runtime.mapaccess1_fast64 call which returns a pointer into the huge
hashmap buffer. This pointer is left on stack in 0x18(rsp).
p := make([]int, N_ELEMS)
is turned into makeslice1 call. 0x18(rsp) refers to makeslice1 return value, which is a
pointer but not initialized until function returns. GC is triggered inside of makeslice1
and it scans 0x18(rsp) completely conservatively.
As the result 160MB hashmap buffer with all the hashes is scanned conservatively.
Questions:
1. We are going to zero function return values and local in C code as well, right?
2. We will need to do the same for assembly functions manually, right?

@lexprfuncall
Copy link

Comment 8:

I do not have definitive answers to your questions.  It is possible to do both 1 and 2
though neither has been discussed.
As an aside, my longer term preference is to not have 1 or 2 and instead have an
explicit way for non-Go code to communicate roots, if needed, to the garbage collector.

@dvyukov
Copy link
Member

dvyukov commented Aug 13, 2013

Comment 9:

Ah, OK, that will do as well.
However, for C manual annotation is nearly impossible (due to amount of code and rate of
changes). But for assembly we can do annotations.

@lexprfuncall
Copy link

Comment 10:

Having the compiler zero memory is something that might be viable in the short term. 
Unfortunately, it has a runtime cost.
One way to avoid the runtime cost is to have only those C and Assembly routines that
store pointers across a call explicitly save any live pointer values in a location known
to the GC across the call.  After the call is completed, the routine can reload the
saved pointer values and make use of them.  I have a write-up of this in progress that I
hope to finish this week.

@randall77
Copy link
Contributor

Comment 11:

This issue was closed by revision 0df438c.

Status changed to Fixed.

@randall77
Copy link
Contributor

Comment 12:

This issue was closed by revision 74e78df.

@rsc
Copy link
Contributor

rsc commented Aug 13, 2013

Comment 13:

Actually it was reopened.

Status changed to Accepted.

@randall77
Copy link
Contributor

Comment 14:

This issue was closed by revision fb37602.

Status changed to Fixed.

@alberts
Copy link
Contributor

alberts commented Sep 4, 2013

Comment 15:

Any chance of applying this fix on the go1.1 branch? Looks a bit chunky maybe.

@ianlancetaylor
Copy link
Member

Comment 16:

We're now working on the Go 1.2 release, there won't be a Go 1.1.3 release.

@alberts
Copy link
Contributor

alberts commented Sep 5, 2013

Comment 17:

Thanks for the feedback.

@rsc rsc added this to the Go1.2 milestone Apr 14, 2015
@rsc rsc removed the go1.2 label Apr 14, 2015
@golang golang locked and limited conversation to collaborators Jun 24, 2016
This issue was closed.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

9 participants