402 lines
8.4 KiB
Go
402 lines
8.4 KiB
Go
|
/*
|
||
|
* Copyright 2020 Dgraph Labs, Inc. and Contributors
|
||
|
*
|
||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
* you may not use this file except in compliance with the License.
|
||
|
* You may obtain a copy of the License at
|
||
|
*
|
||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||
|
*
|
||
|
* Unless required by applicable law or agreed to in writing, software
|
||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
* See the License for the specific language governing permissions and
|
||
|
* limitations under the License.
|
||
|
*/
|
||
|
|
||
|
package z
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"fmt"
|
||
|
"math"
|
||
|
"math/bits"
|
||
|
"math/rand"
|
||
|
"strings"
|
||
|
"sync"
|
||
|
"sync/atomic"
|
||
|
"time"
|
||
|
"unsafe"
|
||
|
|
||
|
"github.com/dustin/go-humanize"
|
||
|
)
|
||
|
|
||
|
// Allocator amortizes the cost of small allocations by allocating memory in
|
||
|
// bigger chunks. Internally it uses z.Calloc to allocate memory. Once
|
||
|
// allocated, the memory is not moved, so it is safe to use the allocated bytes
|
||
|
// to unsafe cast them to Go struct pointers. Maintaining a freelist is slow.
|
||
|
// Instead, Allocator only allocates memory, with the idea that finally we
|
||
|
// would just release the entire Allocator.
|
||
|
type Allocator struct {
|
||
|
sync.Mutex
|
||
|
compIdx uint64 // Stores bufIdx in 32 MSBs and posIdx in 32 LSBs.
|
||
|
buffers [][]byte
|
||
|
Ref uint64
|
||
|
Tag string
|
||
|
}
|
||
|
|
||
|
// allocs keeps references to all Allocators, so we can safely discard them later.
|
||
|
var allocsMu *sync.Mutex
|
||
|
var allocRef uint64
|
||
|
var allocs map[uint64]*Allocator
|
||
|
var calculatedLog2 []int
|
||
|
|
||
|
func init() {
|
||
|
allocsMu = new(sync.Mutex)
|
||
|
allocs = make(map[uint64]*Allocator)
|
||
|
|
||
|
// Set up a unique Ref per process.
|
||
|
rand.Seed(time.Now().UnixNano())
|
||
|
allocRef = uint64(rand.Int63n(1<<16)) << 48
|
||
|
|
||
|
calculatedLog2 = make([]int, 1025)
|
||
|
for i := 1; i <= 1024; i++ {
|
||
|
calculatedLog2[i] = int(math.Log2(float64(i)))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// NewAllocator creates an allocator starting with the given size.
|
||
|
func NewAllocator(sz int) *Allocator {
|
||
|
ref := atomic.AddUint64(&allocRef, 1)
|
||
|
// We should not allow a zero sized page because addBufferWithMinSize
|
||
|
// will run into an infinite loop trying to double the pagesize.
|
||
|
if sz < 512 {
|
||
|
sz = 512
|
||
|
}
|
||
|
a := &Allocator{
|
||
|
Ref: ref,
|
||
|
buffers: make([][]byte, 64),
|
||
|
}
|
||
|
l2 := uint64(log2(sz))
|
||
|
if bits.OnesCount64(uint64(sz)) > 1 {
|
||
|
l2 += 1
|
||
|
}
|
||
|
a.buffers[0] = Calloc(1 << l2)
|
||
|
|
||
|
allocsMu.Lock()
|
||
|
allocs[ref] = a
|
||
|
allocsMu.Unlock()
|
||
|
return a
|
||
|
}
|
||
|
|
||
|
func (a *Allocator) Reset() {
|
||
|
atomic.StoreUint64(&a.compIdx, 0)
|
||
|
}
|
||
|
|
||
|
func Allocators() string {
|
||
|
allocsMu.Lock()
|
||
|
tags := make(map[string]uint64)
|
||
|
num := make(map[string]int)
|
||
|
for _, ac := range allocs {
|
||
|
tags[ac.Tag] += ac.Allocated()
|
||
|
num[ac.Tag] += 1
|
||
|
}
|
||
|
|
||
|
var buf bytes.Buffer
|
||
|
for tag, sz := range tags {
|
||
|
fmt.Fprintf(&buf, "Tag: %s Num: %d Size: %s . ", tag, num[tag], humanize.IBytes(sz))
|
||
|
}
|
||
|
allocsMu.Unlock()
|
||
|
return buf.String()
|
||
|
}
|
||
|
|
||
|
func (a *Allocator) String() string {
|
||
|
var s strings.Builder
|
||
|
s.WriteString(fmt.Sprintf("Allocator: %x\n", a.Ref))
|
||
|
var cum int
|
||
|
for i, b := range a.buffers {
|
||
|
cum += len(b)
|
||
|
if len(b) == 0 {
|
||
|
break
|
||
|
}
|
||
|
s.WriteString(fmt.Sprintf("idx: %d len: %d cum: %d\n", i, len(b), cum))
|
||
|
}
|
||
|
pos := atomic.LoadUint64(&a.compIdx)
|
||
|
bi, pi := parse(pos)
|
||
|
s.WriteString(fmt.Sprintf("bi: %d pi: %d\n", bi, pi))
|
||
|
s.WriteString(fmt.Sprintf("Size: %d\n", a.Size()))
|
||
|
return s.String()
|
||
|
}
|
||
|
|
||
|
// AllocatorFrom would return the allocator corresponding to the ref.
|
||
|
func AllocatorFrom(ref uint64) *Allocator {
|
||
|
allocsMu.Lock()
|
||
|
a := allocs[ref]
|
||
|
allocsMu.Unlock()
|
||
|
return a
|
||
|
}
|
||
|
|
||
|
func parse(pos uint64) (bufIdx, posIdx int) {
|
||
|
return int(pos >> 32), int(pos & 0xFFFFFFFF)
|
||
|
}
|
||
|
|
||
|
// Size returns the size of the allocations so far.
|
||
|
func (a *Allocator) Size() int {
|
||
|
pos := atomic.LoadUint64(&a.compIdx)
|
||
|
bi, pi := parse(pos)
|
||
|
var sz int
|
||
|
for i, b := range a.buffers {
|
||
|
if i < bi {
|
||
|
sz += len(b)
|
||
|
continue
|
||
|
}
|
||
|
sz += pi
|
||
|
return sz
|
||
|
}
|
||
|
panic("Size should not reach here")
|
||
|
}
|
||
|
|
||
|
func log2(sz int) int {
|
||
|
if sz < len(calculatedLog2) {
|
||
|
return calculatedLog2[sz]
|
||
|
}
|
||
|
pow := 10
|
||
|
sz >>= 10
|
||
|
for sz > 1 {
|
||
|
sz >>= 1
|
||
|
pow++
|
||
|
}
|
||
|
return pow
|
||
|
}
|
||
|
|
||
|
func (a *Allocator) Allocated() uint64 {
|
||
|
var alloc int
|
||
|
for _, b := range a.buffers {
|
||
|
alloc += cap(b)
|
||
|
}
|
||
|
return uint64(alloc)
|
||
|
}
|
||
|
|
||
|
func (a *Allocator) TrimTo(max int) {
|
||
|
var alloc int
|
||
|
for i, b := range a.buffers {
|
||
|
if len(b) == 0 {
|
||
|
break
|
||
|
}
|
||
|
alloc += len(b)
|
||
|
if alloc < max {
|
||
|
continue
|
||
|
}
|
||
|
Free(b)
|
||
|
a.buffers[i] = nil
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Release would release the memory back. Remember to make this call to avoid memory leaks.
|
||
|
func (a *Allocator) Release() {
|
||
|
if a == nil {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
var alloc int
|
||
|
for _, b := range a.buffers {
|
||
|
if len(b) == 0 {
|
||
|
break
|
||
|
}
|
||
|
alloc += len(b)
|
||
|
Free(b)
|
||
|
}
|
||
|
|
||
|
allocsMu.Lock()
|
||
|
delete(allocs, a.Ref)
|
||
|
allocsMu.Unlock()
|
||
|
}
|
||
|
|
||
|
const maxAlloc = 1 << 30
|
||
|
|
||
|
func (a *Allocator) MaxAlloc() int {
|
||
|
return maxAlloc
|
||
|
}
|
||
|
|
||
|
const nodeAlign = unsafe.Sizeof(uint64(0)) - 1
|
||
|
|
||
|
func (a *Allocator) AllocateAligned(sz int) []byte {
|
||
|
tsz := sz + int(nodeAlign)
|
||
|
out := a.Allocate(tsz)
|
||
|
// We are reusing allocators. In that case, it's important to zero out the memory allocated
|
||
|
// here. We don't always zero it out (in Allocate), because other functions would be immediately
|
||
|
// overwriting the allocated slices anyway (see Copy).
|
||
|
ZeroOut(out, 0, len(out))
|
||
|
|
||
|
addr := uintptr(unsafe.Pointer(&out[0]))
|
||
|
aligned := (addr + nodeAlign) & ^nodeAlign
|
||
|
start := int(aligned - addr)
|
||
|
|
||
|
return out[start : start+sz]
|
||
|
}
|
||
|
|
||
|
func (a *Allocator) Copy(buf []byte) []byte {
|
||
|
if a == nil {
|
||
|
return append([]byte{}, buf...)
|
||
|
}
|
||
|
out := a.Allocate(len(buf))
|
||
|
copy(out, buf)
|
||
|
return out
|
||
|
}
|
||
|
|
||
|
func (a *Allocator) addBufferAt(bufIdx, minSz int) {
|
||
|
for {
|
||
|
if bufIdx >= len(a.buffers) {
|
||
|
panic(fmt.Sprintf("Allocator can not allocate more than %d buffers", len(a.buffers)))
|
||
|
}
|
||
|
if len(a.buffers[bufIdx]) == 0 {
|
||
|
break
|
||
|
}
|
||
|
if minSz <= len(a.buffers[bufIdx]) {
|
||
|
// No need to do anything. We already have a buffer which can satisfy minSz.
|
||
|
return
|
||
|
}
|
||
|
bufIdx++
|
||
|
}
|
||
|
assert(bufIdx > 0)
|
||
|
// We need to allocate a new buffer.
|
||
|
// Make pageSize double of the last allocation.
|
||
|
pageSize := 2 * len(a.buffers[bufIdx-1])
|
||
|
// Ensure pageSize is bigger than sz.
|
||
|
for pageSize < minSz {
|
||
|
pageSize *= 2
|
||
|
}
|
||
|
// If bigger than maxAlloc, trim to maxAlloc.
|
||
|
if pageSize > maxAlloc {
|
||
|
pageSize = maxAlloc
|
||
|
}
|
||
|
|
||
|
buf := Calloc(pageSize)
|
||
|
assert(len(a.buffers[bufIdx]) == 0)
|
||
|
a.buffers[bufIdx] = buf
|
||
|
}
|
||
|
|
||
|
func (a *Allocator) Allocate(sz int) []byte {
|
||
|
if a == nil {
|
||
|
return make([]byte, sz)
|
||
|
}
|
||
|
if sz > maxAlloc {
|
||
|
panic(fmt.Sprintf("Unable to allocate more than %d\n", maxAlloc))
|
||
|
}
|
||
|
if sz == 0 {
|
||
|
return nil
|
||
|
}
|
||
|
for {
|
||
|
pos := atomic.AddUint64(&a.compIdx, uint64(sz))
|
||
|
bufIdx, posIdx := parse(pos)
|
||
|
buf := a.buffers[bufIdx]
|
||
|
if posIdx > len(buf) {
|
||
|
a.Lock()
|
||
|
newPos := atomic.LoadUint64(&a.compIdx)
|
||
|
newBufIdx, _ := parse(newPos)
|
||
|
if newBufIdx != bufIdx {
|
||
|
a.Unlock()
|
||
|
continue
|
||
|
}
|
||
|
a.addBufferAt(bufIdx+1, sz)
|
||
|
atomic.StoreUint64(&a.compIdx, uint64((bufIdx+1)<<32))
|
||
|
a.Unlock()
|
||
|
// We added a new buffer. Let's acquire slice the right way by going back to the top.
|
||
|
continue
|
||
|
}
|
||
|
data := buf[posIdx-sz : posIdx]
|
||
|
return data
|
||
|
}
|
||
|
}
|
||
|
|
||
|
type AllocatorPool struct {
|
||
|
numGets int64
|
||
|
allocCh chan *Allocator
|
||
|
closer *Closer
|
||
|
}
|
||
|
|
||
|
func NewAllocatorPool(sz int) *AllocatorPool {
|
||
|
a := &AllocatorPool{
|
||
|
allocCh: make(chan *Allocator, sz),
|
||
|
closer: NewCloser(1),
|
||
|
}
|
||
|
go a.freeupAllocators()
|
||
|
return a
|
||
|
}
|
||
|
|
||
|
func (p *AllocatorPool) Get(sz int) *Allocator {
|
||
|
if p == nil {
|
||
|
return NewAllocator(sz)
|
||
|
}
|
||
|
atomic.AddInt64(&p.numGets, 1)
|
||
|
select {
|
||
|
case alloc := <-p.allocCh:
|
||
|
alloc.Reset()
|
||
|
return alloc
|
||
|
default:
|
||
|
return NewAllocator(sz)
|
||
|
}
|
||
|
}
|
||
|
func (p *AllocatorPool) Return(a *Allocator) {
|
||
|
if a == nil {
|
||
|
return
|
||
|
}
|
||
|
if p == nil {
|
||
|
a.Release()
|
||
|
return
|
||
|
}
|
||
|
a.TrimTo(400 << 20)
|
||
|
|
||
|
select {
|
||
|
case p.allocCh <- a:
|
||
|
return
|
||
|
default:
|
||
|
a.Release()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (p *AllocatorPool) Release() {
|
||
|
if p == nil {
|
||
|
return
|
||
|
}
|
||
|
p.closer.SignalAndWait()
|
||
|
}
|
||
|
|
||
|
func (p *AllocatorPool) freeupAllocators() {
|
||
|
defer p.closer.Done()
|
||
|
|
||
|
ticker := time.NewTicker(2 * time.Second)
|
||
|
defer ticker.Stop()
|
||
|
|
||
|
releaseOne := func() bool {
|
||
|
select {
|
||
|
case alloc := <-p.allocCh:
|
||
|
alloc.Release()
|
||
|
return true
|
||
|
default:
|
||
|
return false
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var last int64
|
||
|
for {
|
||
|
select {
|
||
|
case <-p.closer.HasBeenClosed():
|
||
|
close(p.allocCh)
|
||
|
for alloc := range p.allocCh {
|
||
|
alloc.Release()
|
||
|
}
|
||
|
return
|
||
|
|
||
|
case <-ticker.C:
|
||
|
gets := atomic.LoadInt64(&p.numGets)
|
||
|
if gets != last {
|
||
|
// Some retrievals were made since the last time. So, let's avoid doing a release.
|
||
|
last = gets
|
||
|
continue
|
||
|
}
|
||
|
releaseOne()
|
||
|
}
|
||
|
}
|
||
|
}
|