]> Cypherpunks.ru repositories - gostls13.git/commitdiff
arena: add experimental arena package
authorMichael Anthony Knyszek <mknyszek@google.com>
Fri, 12 Aug 2022 23:25:56 +0000 (23:25 +0000)
committerMichael Knyszek <mknyszek@google.com>
Wed, 12 Oct 2022 20:23:36 +0000 (20:23 +0000)
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 <cherryyz@google.com>
Run-TryBot: Michael Knyszek <mknyszek@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>

src/arena/arena.go [new file with mode: 0644]
src/arena/arena_test.go [new file with mode: 0644]
src/go/build/deps_test.go
src/internal/goexperiment/exp_arenas_off.go [new file with mode: 0644]
src/internal/goexperiment/exp_arenas_on.go [new file with mode: 0644]
src/internal/goexperiment/flags.go
src/reflect/arena.go [new file with mode: 0644]
src/runtime/arena.go
src/runtime/arena_test.go
src/runtime/export_test.go
src/runtime/mfinal.go

diff --git a/src/arena/arena.go b/src/arena/arena.go
new file mode 100644 (file)
index 0000000..35b2fbd
--- /dev/null
@@ -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 (file)
index 0000000..017c33c
--- /dev/null
@@ -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)
+       }
+}
index 3da54ba53336ed995379bb8d856d042ef3292719..b7d720a4983cf226731af60ecffd25deaf08c983 100644 (file)
@@ -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 (file)
index 0000000..9e40ebc
--- /dev/null
@@ -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 (file)
index 0000000..92ef748
--- /dev/null
@@ -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
index 8faaf1684d0b0fbac9221855e5293df36bcf1ed2..16793f37ac53811119f44ad5c6fd2f81b7092a85 100644 (file)
@@ -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 (file)
index 0000000..694a3a1
--- /dev/null
@@ -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
index f17a1efaccdfde670844baccb60aebe459230cec..43b133444f6940b0bc27015fb715cb7cc428eaf1 100644 (file)
@@ -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
index c1498afc3e8c10d454bf17f758e86e11fccff2c6..7e121ada714f6465fe6f1e870a7a02313ca05c9a 100644 (file)
@@ -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")
+       }
+}
index 37d7e2f7743baab5da142b757311f28f5f3e87a7..e4b9e362fc166939a6d40d4687e98f12cfd63bbf 100644 (file)
@@ -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
index ef11b7df96dda67dfcb689b4d258e06b67a6940b..a1d08d9293b8d0508bd62dff87e06df5bf1575f5 100644 (file)
@@ -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)