Skip to content

Commit

Permalink
Merge pull request #10 from huandu/feature-arena
Browse files Browse the repository at this point in the history
support arena and add Allocator api
  • Loading branch information
huandu committed Feb 10, 2023
2 parents 6290fc9 + 4c6bc97 commit 0068aca
Show file tree
Hide file tree
Showing 22 changed files with 1,348 additions and 739 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,7 @@ debug_test

# Mac
.DS_Store

# go workspace
go.work
go.work.sum
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

Package `clone` provides functions to deep clone any Go data. It also provides a wrapper to protect a pointer from any unexpected mutation.

For users who use Go 1.18+, it's recommended to import `github.com/huandu/go-clone/generic` for generic APIs.
For users who use Go 1.18+, it's recommended to import `github.com/huandu/go-clone/generic` for generic APIs and arena support.

`Clone`/`Slowly` can clone unexported fields and "no-copy" structs as well. Use this feature wisely.

Expand Down Expand Up @@ -85,6 +85,31 @@ It's required to update minimal Go version to 1.18 to opt-in generic syntax. It

For new users who use Go 1.18+, the generic package is preferred and recommended.

### Arena support

Starting from Go1.20, arena is introduced as a new way to allocate memory. It's quite useful to improve overall performance in special scenarios.
In order to clone a value with memory allocated from an arena, there are new methods `ArenaClone` and `ArenaCloneSlowly` available in `github.com/huandu/go-clone/generic`.

```go
// ArenaClone recursively deep clones v to a new value in arena a.
// It works in the same way as Clone, except it allocates all memory from arena.
func ArenaClone[T any](a *arena.Arena, v T) (nv T)

// ArenaCloneSlowly recursively deep clones v to a new value in arena a.
// It works in the same way as Slowly, except it allocates all memory from arena.
func ArenaCloneSlowly[T any](a *arena.Arena, v T) (nv T)
```

Due to limitations in arena API, memory of the internal data structure of `map` and `chan` is always allocated in heap by Go runtime ([see this issue](https://github.com/golang/go/issues/56230)).

**Warning**: Per [discussion in the arena proposal](https://github.com/golang/go/issues/51317), the arena package may be changed incompatibly or removed in future. All arena related APIs in this package will be changed accordingly.

### Customized allocator

We can control how to allocate memory by creating an `Allocator`. It enables us to take full control over memory allocation when cloning. See [Allocator sample code](https://pkg.go.dev/github.com/huandu/go-clone#example-Allocator) to understand how to use `sync.Pool` in allocator.

For convenience, we can create dedicated allocators for heap or arena by calling `FromHeap()` or `FromArena(a arena.Arena)`.

### Mark struct type as scalar

Some struct types can be considered as scalar.
Expand Down
154 changes: 154 additions & 0 deletions allocator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// Copyright 2023 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.

package clone

import (
"reflect"
"runtime"
"unsafe"
)

var typeOfAllocator = reflect.TypeOf(Allocator{})

// Allocator is a utility type for memory allocation.
type Allocator struct {
pool unsafe.Pointer
new func(pool unsafe.Pointer, t reflect.Type) reflect.Value
makeSlice func(pool unsafe.Pointer, t reflect.Type, len, cap int) reflect.Value
makeMap func(pool unsafe.Pointer, t reflect.Type, n int) reflect.Value
makeChan func(pool unsafe.Pointer, t reflect.Type, buffer int) reflect.Value
}

// AllocatorMethods defines all methods required by allocator.
// If any of these methods is nil, allocator will use default method which allocates memory from heap.
type AllocatorMethods struct {
New func(pool unsafe.Pointer, t reflect.Type) reflect.Value
MakeSlice func(pool unsafe.Pointer, t reflect.Type, len, cap int) reflect.Value
MakeMap func(pool unsafe.Pointer, t reflect.Type, n int) reflect.Value
MakeChan func(pool unsafe.Pointer, t reflect.Type, buffer int) reflect.Value
}

// FromHeap returns an allocator which allocate memory from heap.
func FromHeap() *Allocator {
return heapAllocator
}

// NewAllocator creates an allocator which allocate memory from the pool.
//
// If methods.New is not nil, the allocator itself is created by calling methods.New.
func NewAllocator(pool unsafe.Pointer, methods *AllocatorMethods) (allocator *Allocator) {
if methods.New == nil {
allocator = &Allocator{
pool: pool,
new: methods.New,
makeSlice: methods.MakeSlice,
makeMap: methods.MakeMap,
makeChan: methods.MakeChan,
}
} else {
// Allocate the allocator from the pool.
val := methods.New(pool, typeOfAllocator)
allocator = (*Allocator)(unsafe.Pointer(val.Pointer()))
runtime.KeepAlive(val)

*allocator = Allocator{
pool: pool,
new: methods.New,
makeSlice: methods.MakeSlice,
makeMap: methods.MakeMap,
makeChan: methods.MakeChan,
}
}

if allocator.new == nil {
allocator.new = heapNew
}

if allocator.makeSlice == nil {
allocator.makeSlice = heapMakeSlice
}

if allocator.makeMap == nil {
allocator.makeMap = heapMakeMap
}

if allocator.makeChan == nil {
allocator.makeChan = heapMakeChan
}

return allocator
}

// New returns a new zero value of t.
func (a *Allocator) New(t reflect.Type) reflect.Value {
return a.new(a.pool, t)
}

// MakeSlice creates a new zero-initialized slice value of t with len and cap.
func (a *Allocator) MakeSlice(t reflect.Type, len, cap int) reflect.Value {
return a.makeSlice(a.pool, t, len, cap)
}

// MakeMap creates a new map with minimum size n.
func (a *Allocator) MakeMap(t reflect.Type, n int) reflect.Value {
return a.makeMap(a.pool, t, n)
}

// MakeChan creates a new chan with buffer.
func (a *Allocator) MakeChan(t reflect.Type, buffer int) reflect.Value {
return a.makeChan(a.pool, t, buffer)
}

// Clone recursively deep clone val to a new value with memory allocated from a.
func (a *Allocator) Clone(val reflect.Value) reflect.Value {
if !val.IsValid() {
return val
}

state := &cloneState{
allocator: a,
}
return state.clone(val)
}

// CloneSlowly recursively deep clone val to a new value with memory allocated from a.
// It marks all cloned values internally, thus it can clone v with cycle pointer.
func (a *Allocator) CloneSlowly(val reflect.Value) reflect.Value {
if !val.IsValid() {
return val
}

state := &cloneState{
allocator: a,
visited: visitMap{},
invalid: invalidPointers{},
}
cloned := state.clone(val)
state.fix(cloned)
return cloned
}

// The heapAllocator allocates memory from heap.
var heapAllocator = &Allocator{
new: heapNew,
makeSlice: heapMakeSlice,
makeMap: heapMakeMap,
makeChan: heapMakeChan,
}

func heapNew(pool unsafe.Pointer, t reflect.Type) reflect.Value {
return reflect.New(t)
}

func heapMakeSlice(pool unsafe.Pointer, t reflect.Type, len, cap int) reflect.Value {
return reflect.MakeSlice(t, len, cap)
}

func heapMakeMap(pool unsafe.Pointer, t reflect.Type, n int) reflect.Value {
return reflect.MakeMapWithSize(t, n)
}

func heapMakeChan(pool unsafe.Pointer, t reflect.Type, buffer int) reflect.Value {
return reflect.MakeChan(t, buffer)
}
64 changes: 64 additions & 0 deletions allocator_sample_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright 2023 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.

package clone

import (
"fmt"
"reflect"
"runtime"
"sync"
"unsafe"
)

func ExampleAllocator() {
type Foo struct {
Bar int
}

typeOfFoo := reflect.TypeOf(Foo{})
poolUsed := 0 // For test only.

// A sync pool to allocate Foo.
p := &sync.Pool{
New: func() interface{} {
return &Foo{}
},
}

// Creates a custom allocator using p as pool.
allocator := NewAllocator(unsafe.Pointer(p), &AllocatorMethods{
New: func(pool unsafe.Pointer, t reflect.Type) reflect.Value {
// If t is Foo, allocate value from the sync pool p.
if t == typeOfFoo {
poolUsed++ // For test only.

p := (*sync.Pool)(pool)
v := p.Get()
runtime.SetFinalizer(v, func(v *Foo) {
*v = Foo{}
p.Put(v)
})

return reflect.ValueOf(v)
}

// Fallback to reflect API.
return reflect.New(t)
},
})

// Do clone.
target := []*Foo{
{Bar: 1},
{Bar: 2},
}
cloned := allocator.Clone(reflect.ValueOf(target)).Interface().([]*Foo)

fmt.Println(reflect.DeepEqual(target, cloned))
fmt.Println(poolUsed)

// Output:
// true
// 2
}
86 changes: 86 additions & 0 deletions allocator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright 2023 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.

package clone

import (
"reflect"
"testing"
"unsafe"

"github.com/huandu/go-assert"
)

func TestAllocatorClone(t *testing.T) {
a := assert.New(t)
cnt := 0
allocator := NewAllocator(nil, &AllocatorMethods{
New: func(pool unsafe.Pointer, t reflect.Type) reflect.Value {
cnt++
return heapNew(pool, t)
},
})

type dataNode struct {
Data int
Next *dataNode
}
data := &dataNode{
Data: 1,
Next: &dataNode{
Data: 2,
},
}
cloned := allocator.Clone(reflect.ValueOf(data)).Interface().(*dataNode)
a.Equal(data, cloned)

// Should allocate following value.
// - allocator
// - data
// - data.Next
a.Equal(cnt, 3)
}

func TestAllocatorCloneSlowly(t *testing.T) {
a := assert.New(t)
cnt := 0
allocator := NewAllocator(nil, &AllocatorMethods{
New: func(pool unsafe.Pointer, t reflect.Type) reflect.Value {
cnt++
return heapNew(pool, t)
},
})

type dataNode struct {
Data int
Next *dataNode
}

// data is a cycle linked list.
data := &dataNode{
Data: 1,
Next: &dataNode{
Data: 2,
Next: &dataNode{
Data: 3,
},
},
}
data.Next.Next.Next = data

cloned := allocator.CloneSlowly(reflect.ValueOf(data)).Interface().(*dataNode)

a.Equal(data.Data, cloned.Data)
a.Equal(data.Next.Data, cloned.Next.Data)
a.Equal(data.Next.Next.Data, cloned.Next.Next.Data)
a.Equal(data.Next.Next.Next.Data, cloned.Next.Next.Next.Data)
a.Equal(data.Next.Next.Next.Next.Data, cloned.Next.Next.Next.Next.Data)
a.Assert(cloned.Next.Next.Next == cloned)

// Should allocate following value.
// - allocator
// - data
// - data.Next
// - data.Next.Next
a.Equal(cnt, 4)
}
9 changes: 9 additions & 0 deletions arena.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Copyright 2023 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.

//go:build !(go1.20 && goexperiment.arenas)
// +build !go1.20 !goexperiment.arenas

package clone

const arenaIsEnabled = false
9 changes: 9 additions & 0 deletions arena_go120.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Copyright 2023 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.

//go:build go1.20 && goexperiment.arenas
// +build go1.20,goexperiment.arenas

package clone

const arenaIsEnabled = true
Loading

0 comments on commit 0068aca

Please sign in to comment.