blob: 45959ee8c44b6cd127bb1ecce4cdb3a608a8a762 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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 mallocator
// #include <stdlib.h>
// #include <string.h>
import "C"
import (
"sync"
"sync/atomic"
"unsafe"
)
func roundToPowerOf2(v, round uintptr) uintptr {
forceCarry := round - 1
truncateMask := ^forceCarry
return (v + forceCarry) & truncateMask
}
// Mallocator is an allocator which defers to libc malloc.
//
// The primary reason to use this is when exporting data across the C Data
// Interface. CGO requires that pointers to Go memory are not stored in C
// memory, which is exactly what the C Data Interface would otherwise
// require. By allocating with Mallocator up front, we can safely export the
// buffers in Arrow arrays without copying buffers or violating CGO rules.
//
// The build tag 'mallocator' will also make this the default allocator.
type Mallocator struct {
allocatedBytes uint64
// We want to align allocations, but since we only get/return []byte,
// we need to remember the "real" address for Free somehow
realAllocations sync.Map
alignment int
}
func NewMallocator() *Mallocator { return &Mallocator{alignment: 64} }
func NewMallocatorWithAlignment(alignment int) *Mallocator {
if alignment < 1 {
panic("mallocator: invalid alignment (must be positive)")
} else if alignment > 1 && (alignment&(alignment-1)) != 0 {
panic("mallocator: invalid alignment (must be power of 2)")
}
return &Mallocator{alignment: alignment}
}
func (alloc *Mallocator) Allocate(size int) []byte {
// Use calloc to zero-initialize memory.
// > ...the current implementation may sometimes cause a runtime error if the
// > contents of the C memory appear to be a Go pointer. Therefore, avoid
// > passing uninitialized C memory to Go code if the Go code is going to store
// > pointer values in it. Zero out the memory in C before passing it to Go.
if size < 0 {
panic("mallocator: negative size")
}
paddedSize := C.size_t(size + alloc.alignment)
ptr, err := C.calloc(paddedSize, 1)
if err != nil {
// under some circumstances and allocation patterns, we can end up in a scenario
// where for some reason calloc return ENOMEM even though there is definitely memory
// available for use. So we attempt to fallback to simply doing malloc + memset in
// this case. If malloc returns a nil pointer, then we know we're out of memory
// and will surface the error.
if ptr = C.malloc(paddedSize); ptr == nil {
panic(err)
}
C.memset(ptr, 0, paddedSize)
} else if ptr == nil {
panic("mallocator: out of memory")
}
buf := unsafe.Slice((*byte)(ptr), paddedSize)
aligned := roundToPowerOf2(uintptr(ptr), uintptr(alloc.alignment))
if size == 0 {
// if we return a zero-sized slice, we need to store the actual ptr
// as the key in the map, since the returned slice will have the actual
// pointer value instead of the aligned pointer.
alloc.realAllocations.Store(uintptr(ptr), uintptr(ptr))
} else {
alloc.realAllocations.Store(aligned, uintptr(ptr))
}
atomic.AddUint64(&alloc.allocatedBytes, uint64(size))
if uintptr(ptr) != aligned {
shift := aligned - uintptr(ptr)
return buf[shift : uintptr(size)+shift : uintptr(size)+shift]
}
return buf[:size:size]
}
func (alloc *Mallocator) Free(b []byte) {
sz := len(b)
ptr := getPtr(b)
realAddr, loaded := alloc.realAllocations.LoadAndDelete(uintptr(ptr))
if !loaded {
// double-free?
return
}
realPtr := unsafe.Pointer(realAddr.(uintptr))
C.free(realPtr)
// Subtract sh.Len via two's complement (since atomic doesn't offer subtract)
atomic.AddUint64(&alloc.allocatedBytes, ^(uint64(sz) - 1))
}
func (alloc *Mallocator) Reallocate(size int, b []byte) []byte {
if size < 0 {
panic("mallocator: negative size")
}
if cap(b) >= size {
diff := size - len(b)
atomic.AddUint64(&alloc.allocatedBytes, uint64(diff))
return b[:size]
}
newBuf := alloc.Allocate(size)
copy(newBuf, b)
alloc.Free(b)
return newBuf
}
func (alloc *Mallocator) AllocatedBytes() int64 {
return int64(alloc.allocatedBytes)
}
// Duplicate interface to avoid circular import
type TestingT interface {
Errorf(format string, args ...interface{})
Helper()
}
func (alloc *Mallocator) AssertSize(t TestingT, sz int) {
cur := alloc.AllocatedBytes()
if int64(sz) != cur {
t.Helper()
t.Errorf("invalid memory size exp=%d, got=%d", sz, cur)
}
}