Skip to content

Commit 7836457

Browse files
committed
runtime: add physical memory scavenging test
This change introduces a test to malloc_test which checks for overuse of physical memory in the large object treap. Due to fragmentation, there may be many pages of physical memory that are sitting unused in large-object space. For #14045. Change-Id: I3722468f45063b11246dde6301c7ad02ae34be55 Reviewed-on: https://go-review.googlesource.com/c/138918 Run-TryBot: Michael Knyszek <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Austin Clements <[email protected]>
1 parent c803ffc commit 7836457

File tree

2 files changed

+91
-0
lines changed

2 files changed

+91
-0
lines changed

src/runtime/malloc_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,14 @@ func TestTinyAlloc(t *testing.T) {
168168
}
169169
}
170170

171+
func TestPhysicalMemoryUtilization(t *testing.T) {
172+
got := runTestProg(t, "testprog", "GCPhys")
173+
want := "OK\n"
174+
if got != want {
175+
t.Fatalf("expected %q, but got %q", want, got)
176+
}
177+
}
178+
171179
type acLink struct {
172180
x [1 << 20]byte
173181
}

src/runtime/testdata/testprog/gc.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ func init() {
1717
register("GCFairness", GCFairness)
1818
register("GCFairness2", GCFairness2)
1919
register("GCSys", GCSys)
20+
register("GCPhys", GCPhys)
2021
}
2122

2223
func GCSys() {
@@ -124,3 +125,85 @@ func GCFairness2() {
124125
}
125126
fmt.Println("OK")
126127
}
128+
129+
var maybeSaved []byte
130+
131+
func GCPhys() {
132+
// In this test, we construct a very specific scenario. We first
133+
// allocate N objects and drop half of their pointers on the floor,
134+
// effectively creating N/2 'holes' in our allocated arenas. We then
135+
// try to allocate objects twice as big. At the end, we measure the
136+
// physical memory overhead of large objects.
137+
//
138+
// The purpose of this test is to ensure that the GC scavenges free
139+
// spans eagerly to ensure high physical memory utilization even
140+
// during fragmentation.
141+
const (
142+
// Unfortunately, measuring actual used physical pages is
143+
// difficult because HeapReleased doesn't include the parts
144+
// of an arena that haven't yet been touched. So, we just
145+
// make objects and size sufficiently large such that even
146+
// 64 MB overhead is relatively small in the final
147+
// calculation.
148+
//
149+
// Currently, we target 480MiB worth of memory for our test,
150+
// computed as size * objects + (size*2) * (objects/2)
151+
// = 2 * size * objects
152+
//
153+
// Size must be also large enough to be considered a large
154+
// object (not in any size-segregated span).
155+
size = 1 << 20
156+
objects = 240
157+
)
158+
// Save objects which we want to survive, and condemn objects which we don't.
159+
// Note that we condemn objects in this way and release them all at once in
160+
// order to avoid having the GC start freeing up these objects while the loop
161+
// is still running and filling in the holes we intend to make.
162+
saved := make([][]byte, 0, objects)
163+
condemned := make([][]byte, 0, objects/2+1)
164+
for i := 0; i < objects; i++ {
165+
// Write into a global, to prevent this from being optimized away by
166+
// the compiler in the future.
167+
maybeSaved = make([]byte, size)
168+
if i%2 == 0 {
169+
saved = append(saved, maybeSaved)
170+
} else {
171+
condemned = append(condemned, maybeSaved)
172+
}
173+
}
174+
condemned = nil
175+
// Clean up the heap. This will free up every other object created above
176+
// (i.e. everything in condemned) creating holes in the heap.
177+
runtime.GC()
178+
// Allocate many new objects of 2x size.
179+
for i := 0; i < objects/2; i++ {
180+
saved = append(saved, make([]byte, size*2))
181+
}
182+
// Clean up the heap again just to put it in a known state.
183+
runtime.GC()
184+
// heapBacked is an estimate of the amount of physical memory used by
185+
// this test. HeapSys is an estimate of the size of the mapped virtual
186+
// address space (which may or may not be backed by physical pages)
187+
// whereas HeapReleased is an estimate of the amount of bytes returned
188+
// to the OS. Their difference then roughly corresponds to the amount
189+
// of virtual address space that is backed by physical pages.
190+
var stats runtime.MemStats
191+
runtime.ReadMemStats(&stats)
192+
heapBacked := stats.HeapSys - stats.HeapReleased
193+
// If heapBacked exceeds the amount of memory actually used for heap
194+
// allocated objects by 10% (post-GC HeapAlloc should be quite close to
195+
// the size of the working set), then fail.
196+
//
197+
// In the context of this test, that indicates a large amount of
198+
// fragmentation with physical pages that are otherwise unused but not
199+
// returned to the OS.
200+
overuse := (float64(heapBacked) - float64(stats.HeapAlloc)) / float64(stats.HeapAlloc)
201+
if overuse > 0.1 {
202+
fmt.Printf("exceeded physical memory overuse threshold of 10%%: %3.2f%%\n"+
203+
"(alloc: %d, sys: %d, rel: %d, objs: %d)\n", overuse*100, stats.HeapAlloc,
204+
stats.HeapSys, stats.HeapReleased, len(saved))
205+
return
206+
}
207+
fmt.Println("OK")
208+
runtime.KeepAlive(saved)
209+
}

0 commit comments

Comments
 (0)