Commit acf3ff2e authored by Michael Anthony Knyszek's avatar Michael Anthony Knyszek Committed by Michael Knyszek

runtime: convert page allocator bitmap to sparse array

Currently the page allocator bitmap is implemented as a single giant
memory mapping which is reserved at init time and committed as needed.
This causes problems on systems that don't handle large uncommitted
mappings well, or institute low virtual address space defaults as a
memory limiting mechanism.

This change modifies the implementation of the page allocator bitmap
away from a directly-mapped set of bytes to a sparse array in same vein
as mheap.arenas. This will hurt performance a little but the biggest
gains are from the lockless allocation possible with the page allocator,
so the impact of this extra layer of indirection should be minimal.

In fact, this is exactly what we see:
    https://perf.golang.org/search?q=upload:20191125.5

This reduces the amount of mapped (PROT_NONE) memory needed on systems
with 48-bit address spaces to ~600 MiB down from almost 9 GiB. The bulk
of this remaining memory is used by the summaries.

Go processes with 32-bit address spaces now always commit to 128 KiB of
memory for the bitmap. Previously it would only commit the pages in the
bitmap which represented the range of addresses (lowest address to
highest address, even if there are unused regions in that range) used by
the heap.

Updates #35568.
Updates #35451.

Change-Id: I0ff10380156568642b80c366001eefd0a4e6c762
Reviewed-on: https://go-review.googlesource.com/c/go/+/207497
Run-TryBot: Michael Knyszek <mknyszek@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: default avatarAustin Clements <austin@google.com>
Reviewed-by: default avatarCherry Zhang <cherryyz@google.com>
parent 2ac1ca91
...@@ -355,7 +355,7 @@ func ReadMemStatsSlow() (base, slow MemStats) { ...@@ -355,7 +355,7 @@ func ReadMemStatsSlow() (base, slow MemStats) {
} }
for i := mheap_.pages.start; i < mheap_.pages.end; i++ { for i := mheap_.pages.start; i < mheap_.pages.end; i++ {
pg := mheap_.pages.chunks[i].scavenged.popcntRange(0, pallocChunkPages) pg := mheap_.pages.chunkOf(i).scavenged.popcntRange(0, pallocChunkPages)
slow.HeapReleased += uint64(pg) * pageSize slow.HeapReleased += uint64(pg) * pageSize
} }
for _, p := range allp { for _, p := range allp {
...@@ -726,9 +726,6 @@ func (p *PageAlloc) Free(base, npages uintptr) { ...@@ -726,9 +726,6 @@ func (p *PageAlloc) Free(base, npages uintptr) {
func (p *PageAlloc) Bounds() (ChunkIdx, ChunkIdx) { func (p *PageAlloc) Bounds() (ChunkIdx, ChunkIdx) {
return ChunkIdx((*pageAlloc)(p).start), ChunkIdx((*pageAlloc)(p).end) return ChunkIdx((*pageAlloc)(p).start), ChunkIdx((*pageAlloc)(p).end)
} }
func (p *PageAlloc) PallocData(i ChunkIdx) *PallocData {
return (*PallocData)(&((*pageAlloc)(p).chunks[i]))
}
func (p *PageAlloc) Scavenge(nbytes uintptr, locked bool) (r uintptr) { func (p *PageAlloc) Scavenge(nbytes uintptr, locked bool) (r uintptr) {
systemstack(func() { systemstack(func() {
r = (*pageAlloc)(p).scavenge(nbytes, locked) r = (*pageAlloc)(p).scavenge(nbytes, locked)
...@@ -736,6 +733,16 @@ func (p *PageAlloc) Scavenge(nbytes uintptr, locked bool) (r uintptr) { ...@@ -736,6 +733,16 @@ func (p *PageAlloc) Scavenge(nbytes uintptr, locked bool) (r uintptr) {
return return
} }
// Returns nil if the PallocData's L2 is missing.
func (p *PageAlloc) PallocData(i ChunkIdx) *PallocData {
ci := chunkIdx(i)
l2 := (*pageAlloc)(p).chunks[ci.l1()]
if l2 == nil {
return nil
}
return (*PallocData)(&l2[ci.l2()])
}
// BitRange represents a range over a bitmap. // BitRange represents a range over a bitmap.
type BitRange struct { type BitRange struct {
I, N uint // bit index and length in bits I, N uint // bit index and length in bits
...@@ -769,7 +776,7 @@ func NewPageAlloc(chunks, scav map[ChunkIdx][]BitRange) *PageAlloc { ...@@ -769,7 +776,7 @@ func NewPageAlloc(chunks, scav map[ChunkIdx][]BitRange) *PageAlloc {
p.grow(addr, pallocChunkBytes) p.grow(addr, pallocChunkBytes)
// Initialize the bitmap and update pageAlloc metadata. // Initialize the bitmap and update pageAlloc metadata.
chunk := &p.chunks[chunkIndex(addr)] chunk := p.chunkOf(chunkIndex(addr))
// Clear all the scavenged bits which grow set. // Clear all the scavenged bits which grow set.
chunk.scavenged.clearRange(0, pallocChunkPages) chunk.scavenged.clearRange(0, pallocChunkPages)
...@@ -823,8 +830,13 @@ func FreePageAlloc(pp *PageAlloc) { ...@@ -823,8 +830,13 @@ func FreePageAlloc(pp *PageAlloc) {
} }
// Free the mapped space for chunks. // Free the mapped space for chunks.
chunksLen := uintptr(cap(p.chunks)) * unsafe.Sizeof(p.chunks[0]) for i := range p.chunks {
sysFree(unsafe.Pointer(&p.chunks[0]), alignUp(chunksLen, physPageSize), nil) if x := p.chunks[i]; x != nil {
p.chunks[i] = nil
// This memory comes from sysAlloc and will always be page-aligned.
sysFree(unsafe.Pointer(x), unsafe.Sizeof(*p.chunks[0]), nil)
}
}
} }
// BaseChunkIdx is a convenient chunkIdx value which works on both // BaseChunkIdx is a convenient chunkIdx value which works on both
...@@ -861,7 +873,7 @@ func CheckScavengedBitsCleared(mismatches []BitsMismatch) (n int, ok bool) { ...@@ -861,7 +873,7 @@ func CheckScavengedBitsCleared(mismatches []BitsMismatch) (n int, ok bool) {
lock(&mheap_.lock) lock(&mheap_.lock)
chunkLoop: chunkLoop:
for i := mheap_.pages.start; i < mheap_.pages.end; i++ { for i := mheap_.pages.start; i < mheap_.pages.end; i++ {
chunk := &mheap_.pages.chunks[i] chunk := mheap_.pages.chunkOf(i)
for j := 0; j < pallocChunkPages/64; j++ { for j := 0; j < pallocChunkPages/64; j++ {
// Run over each 64-bit bitmap section and ensure // Run over each 64-bit bitmap section and ensure
// scavenged is being cleared properly on allocation. // scavenged is being cleared properly on allocation.
......
...@@ -420,7 +420,7 @@ func (s *pageAlloc) scavengeOne(max uintptr, locked bool) uintptr { ...@@ -420,7 +420,7 @@ func (s *pageAlloc) scavengeOne(max uintptr, locked bool) uintptr {
// continue if the summary says we can because that's how // continue if the summary says we can because that's how
// we can tell if parts of the address space are unused. // we can tell if parts of the address space are unused.
// See the comment on s.chunks in mpagealloc.go. // See the comment on s.chunks in mpagealloc.go.
base, npages := s.chunks[ci].findScavengeCandidate(chunkPageIndex(s.scavAddr), minPages, maxPages) base, npages := s.chunkOf(ci).findScavengeCandidate(chunkPageIndex(s.scavAddr), minPages, maxPages)
// If we found something, scavenge it and return! // If we found something, scavenge it and return!
if npages != 0 { if npages != 0 {
...@@ -450,8 +450,12 @@ func (s *pageAlloc) scavengeOne(max uintptr, locked bool) uintptr { ...@@ -450,8 +450,12 @@ func (s *pageAlloc) scavengeOne(max uintptr, locked bool) uintptr {
// Run over the chunk looking harder for a candidate. Again, we could // Run over the chunk looking harder for a candidate. Again, we could
// race with a lot of different pieces of code, but we're just being // race with a lot of different pieces of code, but we're just being
// optimistic. // optimistic. Make sure we load the l2 pointer atomically though, to
if !s.chunks[i].hasScavengeCandidate(minPages) { // avoid races with heap growth. It may or may not be possible to also
// see a nil pointer in this case if we do race with heap growth, but
// just defensively ignore the nils. This operation is optimistic anyway.
l2 := (*[1 << pallocChunksL2Bits]pallocData)(atomic.Loadp(unsafe.Pointer(&s.chunks[i.l1()])))
if l2 == nil || !l2[i.l2()].hasScavengeCandidate(minPages) {
continue continue
} }
...@@ -459,7 +463,7 @@ func (s *pageAlloc) scavengeOne(max uintptr, locked bool) uintptr { ...@@ -459,7 +463,7 @@ func (s *pageAlloc) scavengeOne(max uintptr, locked bool) uintptr {
lockHeap() lockHeap()
// Find, verify, and scavenge if we can. // Find, verify, and scavenge if we can.
chunk := &s.chunks[i] chunk := s.chunkOf(i)
base, npages := chunk.findScavengeCandidate(pallocChunkPages-1, minPages, maxPages) base, npages := chunk.findScavengeCandidate(pallocChunkPages-1, minPages, maxPages)
if npages > 0 { if npages > 0 {
// We found memory to scavenge! Mark the bits and report that up. // We found memory to scavenge! Mark the bits and report that up.
...@@ -488,7 +492,7 @@ func (s *pageAlloc) scavengeOne(max uintptr, locked bool) uintptr { ...@@ -488,7 +492,7 @@ func (s *pageAlloc) scavengeOne(max uintptr, locked bool) uintptr {
// //
// s.mheapLock must be held. // s.mheapLock must be held.
func (s *pageAlloc) scavengeRangeLocked(ci chunkIdx, base, npages uint) { func (s *pageAlloc) scavengeRangeLocked(ci chunkIdx, base, npages uint) {
s.chunks[ci].scavenged.setRange(base, npages) s.chunkOf(ci).scavenged.setRange(base, npages)
// Compute the full address for the start of the range. // Compute the full address for the start of the range.
addr := chunkBase(ci) + uintptr(base)*pageSize addr := chunkBase(ci) + uintptr(base)*pageSize
......
This diff is collapsed.
...@@ -26,6 +26,13 @@ const ( ...@@ -26,6 +26,13 @@ const (
// Constants for testing. // Constants for testing.
pageAlloc32Bit = 1 pageAlloc32Bit = 1
pageAlloc64Bit = 0 pageAlloc64Bit = 0
// Number of bits needed to represent all indices into the L1 of the
// chunks map.
//
// See (*pageAlloc).chunks for more details. Update the documentation
// there should this number change.
pallocChunksL1Bits = 0
) )
// See comment in mpagealloc_64bit.go. // See comment in mpagealloc_64bit.go.
......
...@@ -17,6 +17,13 @@ const ( ...@@ -17,6 +17,13 @@ const (
// Constants for testing. // Constants for testing.
pageAlloc32Bit = 0 pageAlloc32Bit = 0
pageAlloc64Bit = 1 pageAlloc64Bit = 1
// Number of bits needed to represent all indices into the L1 of the
// chunks map.
//
// See (*pageAlloc).chunks for more details. Update the documentation
// there should this number change.
pallocChunksL1Bits = 13
) )
// levelBits is the number of bits in the radix for a given level in the super summary // levelBits is the number of bits in the radix for a given level in the super summary
......
...@@ -22,8 +22,14 @@ func checkPageAlloc(t *testing.T, want, got *PageAlloc) { ...@@ -22,8 +22,14 @@ func checkPageAlloc(t *testing.T, want, got *PageAlloc) {
} }
for i := gotStart; i < gotEnd; i++ { for i := gotStart; i < gotEnd; i++ {
// Check the bitmaps. // Check the bitmaps. Note that we may have nil data.
gb, wb := got.PallocData(i), want.PallocData(i) gb, wb := got.PallocData(i), want.PallocData(i)
if gb == nil && wb == nil {
continue
}
if (gb == nil && wb != nil) || (gb != nil && wb == nil) {
t.Errorf("chunk %d nilness mismatch", i)
}
if !checkPallocBits(t, gb.PallocBits(), wb.PallocBits()) { if !checkPallocBits(t, gb.PallocBits(), wb.PallocBits()) {
t.Logf("in chunk %d (mallocBits)", i) t.Logf("in chunk %d (mallocBits)", i)
} }
......
...@@ -83,10 +83,10 @@ func (c *pageCache) flush(s *pageAlloc) { ...@@ -83,10 +83,10 @@ func (c *pageCache) flush(s *pageAlloc) {
// slower, safer thing by iterating over each bit individually. // slower, safer thing by iterating over each bit individually.
for i := uint(0); i < 64; i++ { for i := uint(0); i < 64; i++ {
if c.cache&(1<<i) != 0 { if c.cache&(1<<i) != 0 {
s.chunks[ci].free1(pi + i) s.chunkOf(ci).free1(pi + i)
} }
if c.scav&(1<<i) != 0 { if c.scav&(1<<i) != 0 {
s.chunks[ci].scavenged.setRange(pi+i, 1) s.chunkOf(ci).scavenged.setRange(pi+i, 1)
} }
} }
// Since this is a lot like a free, we need to make sure // Since this is a lot like a free, we need to make sure
...@@ -113,14 +113,15 @@ func (s *pageAlloc) allocToCache() pageCache { ...@@ -113,14 +113,15 @@ func (s *pageAlloc) allocToCache() pageCache {
ci := chunkIndex(s.searchAddr) // chunk index ci := chunkIndex(s.searchAddr) // chunk index
if s.summary[len(s.summary)-1][ci] != 0 { if s.summary[len(s.summary)-1][ci] != 0 {
// Fast path: there's free pages at or near the searchAddr address. // Fast path: there's free pages at or near the searchAddr address.
j, _ := s.chunks[ci].find(1, chunkPageIndex(s.searchAddr)) chunk := s.chunkOf(ci)
j, _ := chunk.find(1, chunkPageIndex(s.searchAddr))
if j < 0 { if j < 0 {
throw("bad summary data") throw("bad summary data")
} }
c = pageCache{ c = pageCache{
base: chunkBase(ci) + alignDown(uintptr(j), 64)*pageSize, base: chunkBase(ci) + alignDown(uintptr(j), 64)*pageSize,
cache: ^s.chunks[ci].pages64(j), cache: ^chunk.pages64(j),
scav: s.chunks[ci].scavenged.block64(j), scav: chunk.scavenged.block64(j),
} }
} else { } else {
// Slow path: the searchAddr address had nothing there, so go find // Slow path: the searchAddr address had nothing there, so go find
...@@ -133,10 +134,11 @@ func (s *pageAlloc) allocToCache() pageCache { ...@@ -133,10 +134,11 @@ func (s *pageAlloc) allocToCache() pageCache {
return pageCache{} return pageCache{}
} }
ci := chunkIndex(addr) ci := chunkIndex(addr)
chunk := s.chunkOf(ci)
c = pageCache{ c = pageCache{
base: alignDown(addr, 64*pageSize), base: alignDown(addr, 64*pageSize),
cache: ^s.chunks[ci].pages64(chunkPageIndex(addr)), cache: ^chunk.pages64(chunkPageIndex(addr)),
scav: s.chunks[ci].scavenged.block64(chunkPageIndex(addr)), scav: chunk.scavenged.block64(chunkPageIndex(addr)),
} }
} }
......
...@@ -369,6 +369,9 @@ func findBitRange64(c uint64, n uint) uint { ...@@ -369,6 +369,9 @@ func findBitRange64(c uint64, n uint) uint {
// whether or not a given page is scavenged in a single // whether or not a given page is scavenged in a single
// structure. It's effectively a pallocBits with // structure. It's effectively a pallocBits with
// additional functionality. // additional functionality.
//
// Update the comment on (*pageAlloc).chunks should this
// structure change.
type pallocData struct { type pallocData struct {
pallocBits pallocBits
scavenged pageBits scavenged pageBits
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment