From: Michael Anthony Knyszek Date: Fri, 12 Aug 2022 23:25:56 +0000 (+0000) Subject: arena: add experimental arena package X-Git-Tag: go1.20rc1~661 X-Git-Url: http://www.git.cypherpunks.su/?a=commitdiff_plain;h=e0d01b8467b5cb9e68758932f50c3187374011ba;p=gostls13.git arena: add experimental arena package This change adds the arenas package and a function to reflect for allocating from an arena via reflection, but all the new API is placed behind a GOEXPERIMENT. For #51317. Change-Id: I026d46294e26ab386d74625108c19a0024fbcedc Reviewed-on: https://go-review.googlesource.com/c/go/+/423361 Reviewed-by: Cherry Mui Run-TryBot: Michael Knyszek TryBot-Result: Gopher Robot --- diff --git a/src/arena/arena.go b/src/arena/arena.go new file mode 100644 index 0000000000..35b2fbd2aa --- /dev/null +++ b/src/arena/arena.go @@ -0,0 +1,108 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build goexperiment.arenas + +/* +The arena package provides the ability to allocate memory for a collection +of Go values and free that space manually all at once, safely. The purpose +of this functionality is to improve efficiency: manually freeing memory +before a garbage collection delays that cycle. Less frequent cycles means +the CPU cost of the garbage collector is incurred less frequently. + +This functionality in this package is mostly captured in the Arena type. +Arenas allocate large chunks of memory for Go values, so they're likely to +be inefficient for allocating only small amounts of small Go values. They're +best used in bulk, on the order of MiB of memory allocated on each use. + +Note that by allowing for this limited form of manual memory allocation +that use-after-free bugs are possible with regular Go values. This package +limits the impact of these use-after-free bugs by preventing reuse of freed +memory regions until the garbage collector is able to determine that it is +safe. Typically, a use-after-free bug will result in a fault and a helpful +error message, but this package reserves the right to not force a fault on +freed memory. That means a valid implementation of this package is to just +allocate all memory the way the runtime normally would, and in fact, it +reserves the right to occasionally do so for some Go values. +*/ +package arena + +import ( + "internal/reflectlite" + "unsafe" +) + +// Arena represents a collection of Go values allocated and freed together. +// Arenas are useful for improving efficiency as they may be freed back to +// the runtime manually, though any memory obtained from freed arenas must +// not be accessed once that happens. An Arena is automatically freed once +// it is no longer referenced, so it must be kept alive (see runtime.KeepAlive) +// until any memory allocated from it is no longer needed. +// +// An Arena must never be used concurrently by multiple goroutines. +type Arena struct { + a unsafe.Pointer +} + +// NewArena allocates a new arena. +func NewArena() *Arena { + return &Arena{a: runtime_arena_newArena()} +} + +// Free frees the arena (and all objects allocated from the arena) so that +// memory backing the arena can be reused fairly quickly without garbage +// collection overhead. Applications must not call any method on this +// arena after it has been freed. +func (a *Arena) Free() { + runtime_arena_arena_Free(a.a) + a.a = nil +} + +// New creates a new *T in the provided arena. The *T must not be used after +// the arena is freed. Accessing the value after free may result in a fault, +// but this fault is also not guaranteed. +func New[T any](a *Arena) *T { + return runtime_arena_arena_New(a.a, reflectlite.TypeOf((*T)(nil))).(*T) +} + +// MakeSlice creates a new []T with the provided capacity and length. The []T must +// not be used after the arena is freed. Accessing the underlying storage of the +// slice after free may result in a fault, but this fault is also not guaranteed. +func MakeSlice[T any](a *Arena, len, cap int) []T { + var sl []T + runtime_arena_arena_Slice(a.a, &sl, cap) + return sl[:len] +} + +// Clone makes a shallow copy of the input value that is no longer bound to any +// arena it may have been allocated from, returning the copy. If it was not +// allocated from an arena, it is returned untouched. This function is useful +// to more easily let an arena-allocated value out-live its arena. +// T must be a pointer, a slice, or a string, otherwise this function will panic. +func Clone[T any](s T) T { + return runtime_arena_heapify(s).(T) +} + +//go:linkname reflect_arena_New reflect.arena_New +func reflect_arena_New(a *Arena, typ any) any { + return runtime_arena_arena_New(a.a, typ) +} + +//go:linkname runtime_arena_newArena +func runtime_arena_newArena() unsafe.Pointer + +//go:linkname runtime_arena_arena_New +func runtime_arena_arena_New(arena unsafe.Pointer, typ any) any + +// Mark as noescape to avoid escaping the slice header. +// +//go:noescape +//go:linkname runtime_arena_arena_Slice +func runtime_arena_arena_Slice(arena unsafe.Pointer, slice any, cap int) + +//go:linkname runtime_arena_arena_Free +func runtime_arena_arena_Free(arena unsafe.Pointer) + +//go:linkname runtime_arena_heapify +func runtime_arena_heapify(any) any diff --git a/src/arena/arena_test.go b/src/arena/arena_test.go new file mode 100644 index 0000000000..017c33c502 --- /dev/null +++ b/src/arena/arena_test.go @@ -0,0 +1,42 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build goexperiment.arenas + +package arena_test + +import ( + "arena" + "testing" +) + +type T1 struct { + n int +} +type T2 [1 << 20]byte // 1MiB + +func TestSmoke(t *testing.T) { + a := arena.NewArena() + defer a.Free() + + tt := arena.New[T1](a) + tt.n = 1 + + ts := arena.MakeSlice[T1](a, 99, 100) + if len(ts) != 99 { + t.Errorf("Slice() len = %d, want 99", len(ts)) + } + if cap(ts) != 100 { + t.Errorf("Slice() cap = %d, want 100", cap(ts)) + } + ts[1].n = 42 +} + +func TestSmokeLarge(t *testing.T) { + a := arena.NewArena() + defer a.Free() + for i := 0; i < 10*64; i++ { + _ = arena.New[T2](a) + } +} diff --git a/src/go/build/deps_test.go b/src/go/build/deps_test.go index 3da54ba533..b7d720a498 100644 --- a/src/go/build/deps_test.go +++ b/src/go/build/deps_test.go @@ -78,6 +78,9 @@ var depsRules = ` RUNTIME < io; + RUNTIME + < arena; + syscall !< io; reflect !< sort; @@ -165,7 +168,7 @@ var depsRules = ` # FMT is OS (which includes string routines) plus reflect and fmt. # It does not include package log, which should be avoided in core packages. - strconv, unicode + arena, strconv, unicode < reflect; os, reflect diff --git a/src/internal/goexperiment/exp_arenas_off.go b/src/internal/goexperiment/exp_arenas_off.go new file mode 100644 index 0000000000..9e40ebc37b --- /dev/null +++ b/src/internal/goexperiment/exp_arenas_off.go @@ -0,0 +1,9 @@ +// Code generated by mkconsts.go. DO NOT EDIT. + +//go:build !goexperiment.arenas +// +build !goexperiment.arenas + +package goexperiment + +const Arenas = false +const ArenasInt = 0 diff --git a/src/internal/goexperiment/exp_arenas_on.go b/src/internal/goexperiment/exp_arenas_on.go new file mode 100644 index 0000000000..92ef748f62 --- /dev/null +++ b/src/internal/goexperiment/exp_arenas_on.go @@ -0,0 +1,9 @@ +// Code generated by mkconsts.go. DO NOT EDIT. + +//go:build goexperiment.arenas +// +build goexperiment.arenas + +package goexperiment + +const Arenas = true +const ArenasInt = 1 diff --git a/src/internal/goexperiment/flags.go b/src/internal/goexperiment/flags.go index 8faaf1684d..16793f37ac 100644 --- a/src/internal/goexperiment/flags.go +++ b/src/internal/goexperiment/flags.go @@ -90,4 +90,8 @@ type Flags struct { // CoverageRedesign enables the new compiler-based code coverage // tooling. CoverageRedesign bool + + // Arenas causes the "arena" standard library package to be visible + // to the outside world. + Arenas bool } diff --git a/src/reflect/arena.go b/src/reflect/arena.go new file mode 100644 index 0000000000..694a3a136c --- /dev/null +++ b/src/reflect/arena.go @@ -0,0 +1,18 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build goexperiment.arenas + +package reflect + +import "arena" + +// ArenaNew returns a Value representing a pointer to a new zero value for the +// specified type, allocating storage for it in the provided arena. That is, +// the returned Value's Type is PointerTo(typ). +func ArenaNew(a *arena.Arena, typ Type) Value { + return ValueOf(arena_New(a, typ)) +} + +func arena_New(a *arena.Arena, typ any) any diff --git a/src/runtime/arena.go b/src/runtime/arena.go index f17a1efacc..43b133444f 100644 --- a/src/runtime/arena.go +++ b/src/runtime/arena.go @@ -89,6 +89,102 @@ import ( "unsafe" ) +// Functions starting with arena_ are meant to be exported to downstream users +// of arenas. They should wrap these functions in a higher-lever API. +// +// The underlying arena and its resources are managed through an opaque unsafe.Pointer. + +// arena_newArena is a wrapper around newUserArena. +// +//go:linkname arena_newArena arena.runtime_arena_newArena +func arena_newArena() unsafe.Pointer { + return unsafe.Pointer(newUserArena()) +} + +// arena_arena_New is a wrapper around (*userArena).new, except that typ +// is an any (must be a *_type, still) and typ must be a type descriptor +// for a pointer to the type to actually be allocated, i.e. pass a *T +// to allocate a T. This is necessary because this function returns a *T. +// +//go:linkname arena_arena_New arena.runtime_arena_arena_New +func arena_arena_New(arena unsafe.Pointer, typ any) any { + t := (*_type)(efaceOf(&typ).data) + if t.kind&kindMask != kindPtr { + throw("arena_New: non-pointer type") + } + te := (*ptrtype)(unsafe.Pointer(t)).elem + x := ((*userArena)(arena)).new(te) + var result any + e := efaceOf(&result) + e._type = t + e.data = x + return result +} + +// arena_arena_Slice is a wrapper around (*userArena).slice. +// +//go:linkname arena_arena_Slice arena.runtime_arena_arena_Slice +func arena_arena_Slice(arena unsafe.Pointer, slice any, cap int) { + ((*userArena)(arena)).slice(slice, cap) +} + +// arena_arena_Free is a wrapper around (*userArena).free. +// +//go:linkname arena_arena_Free arena.runtime_arena_arena_Free +func arena_arena_Free(arena unsafe.Pointer) { + ((*userArena)(arena)).free() +} + +// arena_heapify takes a value that lives in an arena and makes a copy +// of it on the heap. Values that don't live in an arena are returned unmodified. +// +//go:linkname arena_heapify arena.runtime_arena_heapify +func arena_heapify(s any) any { + var v unsafe.Pointer + e := efaceOf(&s) + t := e._type + switch t.kind & kindMask { + case kindString: + v = stringStructOf((*string)(e.data)).str + case kindSlice: + v = (*slice)(e.data).array + case kindPtr: + v = e.data + default: + panic("arena: Clone only supports pointers, slices, and strings") + } + span := spanOf(uintptr(v)) + if span == nil || !span.isUserArenaChunk { + // Not stored in a user arena chunk. + return s + } + // Heap-allocate storage for a copy. + var x any + switch t.kind & kindMask { + case kindString: + s1 := s.(string) + s2, b := rawstring(len(s1)) + copy(b, s1) + x = s2 + case kindSlice: + len := (*slice)(e.data).len + et := (*slicetype)(unsafe.Pointer(t)).elem + sl := new(slice) + *sl = slice{makeslicecopy(et, len, len, (*slice)(e.data).array), len, len} + xe := efaceOf(&x) + xe._type = t + xe.data = unsafe.Pointer(sl) + case kindPtr: + et := (*ptrtype)(unsafe.Pointer(t)).elem + e2 := newobject(et) + typedmemmove(et, e2, e.data) + xe := efaceOf(&x) + xe._type = t + xe.data = e2 + } + return x +} + const ( // userArenaChunkBytes is the size of a user arena chunk. userArenaChunkBytesMax = 8 << 20 diff --git a/src/runtime/arena_test.go b/src/runtime/arena_test.go index c1498afc3e..7e121ada71 100644 --- a/src/runtime/arena_test.go +++ b/src/runtime/arena_test.go @@ -375,3 +375,155 @@ func TestUserArenaClearsPointerBits(t *testing.T) { GC() GC() } + +func TestUserArenaCloneString(t *testing.T) { + a := NewUserArena() + + // A static string (not on heap or arena) + var s = "abcdefghij" + + // Create a byte slice in the arena, initialize it with s + var b []byte + a.Slice(&b, len(s)) + copy(b, s) + + // Create a string as using the same memory as the byte slice, hence in + // the arena. This could be an arena API, but hasn't really been needed + // yet. + var as string + asHeader := (*reflect.StringHeader)(unsafe.Pointer(&as)) + asHeader.Data = (*reflect.SliceHeader)(unsafe.Pointer(&b)).Data + asHeader.Len = len(b) + + // Clone should make a copy of as, since it is in the arena. + asCopy := UserArenaClone(as) + if (*reflect.StringHeader)(unsafe.Pointer(&as)).Data == (*reflect.StringHeader)(unsafe.Pointer(&asCopy)).Data { + t.Error("Clone did not make a copy") + } + + // Clone should make a copy of subAs, since subAs is just part of as and so is in the arena. + subAs := as[1:3] + subAsCopy := UserArenaClone(subAs) + if (*reflect.StringHeader)(unsafe.Pointer(&subAs)).Data == (*reflect.StringHeader)(unsafe.Pointer(&subAsCopy)).Data { + t.Error("Clone did not make a copy") + } + if len(subAs) != len(subAsCopy) { + t.Errorf("Clone made an incorrect copy (bad length): %d -> %d", len(subAs), len(subAsCopy)) + } else { + for i := range subAs { + if subAs[i] != subAsCopy[i] { + t.Errorf("Clone made an incorrect copy (data at index %d): %d -> %d", i, subAs[i], subAs[i]) + } + } + } + + // Clone should not make a copy of doubleAs, since doubleAs will be on the heap. + doubleAs := as + as + doubleAsCopy := UserArenaClone(doubleAs) + if (*reflect.StringHeader)(unsafe.Pointer(&doubleAs)).Data != (*reflect.StringHeader)(unsafe.Pointer(&doubleAsCopy)).Data { + t.Error("Clone should not have made a copy") + } + + // Clone should not make a copy of s, since s is a static string. + sCopy := UserArenaClone(s) + if (*reflect.StringHeader)(unsafe.Pointer(&s)).Data != (*reflect.StringHeader)(unsafe.Pointer(&sCopy)).Data { + t.Error("Clone should not have made a copy") + } + + a.Free() +} + +func TestUserArenaClonePointer(t *testing.T) { + a := NewUserArena() + + // Clone should not make a copy of a heap-allocated smallScalar. + x := Escape(new(smallScalar)) + xCopy := UserArenaClone(x) + if unsafe.Pointer(x) != unsafe.Pointer(xCopy) { + t.Errorf("Clone should not have made a copy: %#v -> %#v", x, xCopy) + } + + // Clone should make a copy of an arena-allocated smallScalar. + var i any + i = (*smallScalar)(nil) + a.New(&i) + xArena := i.(*smallScalar) + xArenaCopy := UserArenaClone(xArena) + if unsafe.Pointer(xArena) == unsafe.Pointer(xArenaCopy) { + t.Errorf("Clone should have made a copy: %#v -> %#v", xArena, xArenaCopy) + } + if *xArena != *xArenaCopy { + t.Errorf("Clone made an incorrect copy copy: %#v -> %#v", *xArena, *xArenaCopy) + } + + a.Free() +} + +func TestUserArenaCloneSlice(t *testing.T) { + a := NewUserArena() + + // A static string (not on heap or arena) + var s = "klmnopqrstuv" + + // Create a byte slice in the arena, initialize it with s + var b []byte + a.Slice(&b, len(s)) + copy(b, s) + + // Clone should make a copy of b, since it is in the arena. + bCopy := UserArenaClone(b) + if unsafe.Pointer(&b[0]) == unsafe.Pointer(&bCopy[0]) { + t.Errorf("Clone did not make a copy: %#v -> %#v", b, bCopy) + } + if len(b) != len(bCopy) { + t.Errorf("Clone made an incorrect copy (bad length): %d -> %d", len(b), len(bCopy)) + } else { + for i := range b { + if b[i] != bCopy[i] { + t.Errorf("Clone made an incorrect copy (data at index %d): %d -> %d", i, b[i], bCopy[i]) + } + } + } + + // Clone should make a copy of bSub, since bSub is just part of b and so is in the arena. + bSub := b[1:3] + bSubCopy := UserArenaClone(bSub) + if unsafe.Pointer(&bSub[0]) == unsafe.Pointer(&bSubCopy[0]) { + t.Errorf("Clone did not make a copy: %#v -> %#v", bSub, bSubCopy) + } + if len(bSub) != len(bSubCopy) { + t.Errorf("Clone made an incorrect copy (bad length): %d -> %d", len(bSub), len(bSubCopy)) + } else { + for i := range bSub { + if bSub[i] != bSubCopy[i] { + t.Errorf("Clone made an incorrect copy (data at index %d): %d -> %d", i, bSub[i], bSubCopy[i]) + } + } + } + + // Clone should not make a copy of bNotArena, since it will not be in an arena. + bNotArena := make([]byte, len(s)) + copy(bNotArena, s) + bNotArenaCopy := UserArenaClone(bNotArena) + if unsafe.Pointer(&bNotArena[0]) != unsafe.Pointer(&bNotArenaCopy[0]) { + t.Error("Clone should not have made a copy") + } + + a.Free() +} + +func TestUserArenaClonePanic(t *testing.T) { + var s string + func() { + x := smallScalar{2} + defer func() { + if v := recover(); v != nil { + s = v.(string) + } + }() + UserArenaClone(x) + }() + if s == "" { + t.Errorf("expected panic from Clone") + } +} diff --git a/src/runtime/export_test.go b/src/runtime/export_test.go index 37d7e2f774..e4b9e362fc 100644 --- a/src/runtime/export_test.go +++ b/src/runtime/export_test.go @@ -1671,6 +1671,10 @@ func GlobalWaitingArenaChunks() int { return n } +func UserArenaClone[T any](s T) T { + return arena_heapify(s).(T) +} + var AlignUp = alignUp // BlockUntilEmptyFinalizerQueue blocks until either the finalizer diff --git a/src/runtime/mfinal.go b/src/runtime/mfinal.go index ef11b7df96..a1d08d9293 100644 --- a/src/runtime/mfinal.go +++ b/src/runtime/mfinal.go @@ -370,6 +370,11 @@ func SetFinalizer(obj any, finalizer any) { throw("nil elem type!") } + if inUserArenaChunk(uintptr(e.data)) { + // Arena-allocated objects are not eligible for finalizers. + throw("runtime.SetFinalizer: first argument was allocated into an arena") + } + // find the containing object base, _, _ := findObject(uintptr(e.data), 0, 0)