// Copyright 2017 The Memory Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Package memory implements a memory allocator. // // # Build status // // available at https://modern-c.appspot.com/-/builder/?importpath=modernc.org%2fmemory // // # Changelog // // 2017-10-03 Added alternative, unsafe.Pointer-based API. // // Package memory implements a memory allocator. // // # Changelog // // 2017-10-03 Added alternative, unsafe.Pointer-based API. // // # Page retention // // Memory is acquired from the OS in 64 KiB units or, for larger // allocations, in units rounded up to a power of two on 64-bit hosts. // An Allocator retains empty regions for reuse instead of unmapping // them as soon as their last slot is freed: each size class keeps its // current carve-target page, and beyond that, retired regions of any // size go into a pool bounded by 4 MiB plus the high-water mark of the // live mapping. An allocation pattern that repeatedly drains a size // class, or repeatedly frees and reallocates large buffers, no longer // pays a mmap/munmap round trip per turnover. Use Allocator.Trim to // hand the retained regions back. // // # Benchmarks // // jnml@3900x:~/src/modernc.org/memory$ date ; go version ; go test -run @ -bench . -benchmem |& tee log // Mon Sep 25 16:02:02 CEST 2023 // go version go1.21.1 linux/amd64 // goos: linux // goarch: amd64 // pkg: modernc.org/memory // cpu: AMD Ryzen 9 3900X 12-Core Processor // BenchmarkFree16-24 123506772 9.802 ns/op 0 B/op 0 allocs/op // BenchmarkFree32-24 73853230 15.08 ns/op 0 B/op 0 allocs/op // BenchmarkFree64-24 43070334 25.15 ns/op 0 B/op 0 allocs/op // BenchmarkCalloc16-24 59353304 18.92 ns/op 0 B/op 0 allocs/op // BenchmarkCalloc32-24 39415004 29.00 ns/op 0 B/op 0 allocs/op // BenchmarkCalloc64-24 35825725 32.02 ns/op 0 B/op 0 allocs/op // BenchmarkGoCalloc16-24 38274313 26.99 ns/op 16 B/op 1 allocs/op // BenchmarkGoCalloc32-24 44590477 33.06 ns/op 32 B/op 1 allocs/op // BenchmarkGoCalloc64-24 44233016 37.20 ns/op 64 B/op 1 allocs/op // BenchmarkMalloc16-24 145736911 7.720 ns/op 0 B/op 0 allocs/op // BenchmarkMalloc32-24 128898334 7.887 ns/op 0 B/op 0 allocs/op // BenchmarkMalloc64-24 149569483 7.994 ns/op 0 B/op 0 allocs/op // BenchmarkUintptrFree16-24 117043012 9.205 ns/op 0 B/op 0 allocs/op // BenchmarkUintptrFree32-24 77399617 14.20 ns/op 0 B/op 0 allocs/op // BenchmarkUintptrFree64-24 48770785 25.04 ns/op 0 B/op 0 allocs/op // BenchmarkUintptrCalloc16-24 79257636 15.44 ns/op 0 B/op 0 allocs/op // BenchmarkUintptrCalloc32-24 49644562 23.62 ns/op 0 B/op 0 allocs/op // BenchmarkUintptrCalloc64-24 39854710 28.22 ns/op 0 B/op 0 allocs/op // BenchmarkUintptrMalloc16-24 252987727 4.525 ns/op 0 B/op 0 allocs/op // BenchmarkUintptrMalloc32-24 241423840 4.433 ns/op 0 B/op 0 allocs/op // BenchmarkUintptrMalloc64-24 256450324 4.669 ns/op 0 B/op 0 allocs/op // PASS // ok modernc.org/memory 93.178s // jnml@3900x:~/src/modernc.org/memory$ package memory // import "modernc.org/memory" import ( "fmt" "math/bits" "os" "unsafe" ) const ( headerSize = unsafe.Sizeof(page{}) mallocAllign = 2 * unsafe.Sizeof(uintptr(0)) maxSlotSize = 1 << maxSlotSizeLog maxSlotSizeLog = pageSizeLog - 2 pageAvail = pageSize - headerSize pageMask = pageSize - 1 pageSize = 1 << pageSizeLog ) func init() { if unsafe.Sizeof(page{})%mallocAllign != 0 { panic("internal error") } } // if n%m != 0 { n += m-n%m }. m must be a power of 2. func roundup(n, m int) int { return (n + m - 1) &^ (m - 1) } type node struct { prev, next uintptr // *node } type page struct { brk int log uint size int used int } // Allocator allocates and frees memory. Its zero value is ready for use. The // exported counters are updated only when build tag memory.counters is // present. // // An Allocator retains empty regions for reuse, see Trim. type Allocator struct { Allocs int // # of allocs. Bytes int // Asked from OS. cap [64]int lists [64]uintptr // *node Mmaps int // Asked from OS. pages [64]uintptr // *page regs map[uintptr]struct{} // map[*page]struct{} freed map[int][]uintptr // empty regions retained for reuse, keyed by region size freedSize int // total bytes in freed cold map[int]int // the bottom cold[size] regions of freed[size] are decommitted live int // bytes mapped and not in freed hiLive int // high-water mark of live } // maxFreedSize returns the bound on the total bytes retained in // a.freed: a fixed floor plus the high-water mark of the live mapping. // The high-water mark rather than the current live size matters for // large transient buffers: when one is freed, live has already shrunk // by the buffer, and it is exactly the region most worth retaining. func (a *Allocator) maxFreedSize() int { return 4<<20 + a.hiLive } // addLive adds n bytes to the live mapping size, tracking its // high-water mark. func (a *Allocator) addLive(n int) { a.live += n if a.live > a.hiLive { a.hiLive = a.live } } // canRetain reports whether a.freed has room to retain size more bytes. func (a *Allocator) canRetain(size int) bool { return a.freedSize+size <= a.maxFreedSize() } // mmapSize returns the region size class that backs a request for size // bytes: requests up to pageSize share the single pageSize class, and // on 64-bit larger requests are rounded up to a power of two. The // extra address space is never touched and so costs nothing, and // collapsing sizes into classes makes reuse via a.freed much more // likely. // // Classes are multiples of mmapGranularity - the granularity the platform // mmap actually maps at - so the class of a request agrees with the // page.size the mapping comes back with, which is what retain later keys // the pool by. Rounding to anything finer describes a mapping the platform // cannot make: on windows/386 a 1 MiB+1 request rounded to OS pages misses // forever the 1 MiB+64 KiB region its own mapping put into the pool. func mmapSize(size int) int { switch { case size <= pageSize && pageSize%osPageSize == 0: return pageSize case size > pageSize && bits.UintSize == 64: return roundup(1<= len(s) { // popping from the decommitted prefix if err := recommit(p, size); err != nil { return 0, err // pool left untouched } a.cold[size] = len(s) - 1 } } a.freed[size] = s[:len(s)-1] a.freedSize -= size a.addLive(size) pg := (*page)(unsafe.Pointer(p)) pg.brk = 0 pg.used = 0 return p, nil } // Aligning a fresh mapping to a pageSize boundary costs up to two // munmap calls for the trimmed-off ends, so when a single pageSize // region is needed, acquire a batch in one mapping and put the // rest into the pool: three syscalls amortized over slabBatch // regions instead of per region. if canCarve && size == pageSize { if p, n, err := mmap(slabBatch * pageSize); err == nil { if n%pageSize == 0 && a.canRetain(n-pageSize) { for off := pageSize; off < n; off += pageSize { q := p + uintptr(off) a.reg(q, pageSize) a.retain(q, pageSize) } return a.reg(p, pageSize), nil } if err := unmap(p, n); err != nil { return 0, err } } // Fall through and map a single region. } p, size, err := mmap(size) if err != nil { return 0, err } //TODO(jnml) The returned size may now be nearly as twice as large as we asked //for. Use that extra capacity. For that we need to move the respective //Allocator.cap item into the page struct so the page cap becomes dynamic. // // Related: This is a consequence of fixing the bigsort.test failures on // linux/s390x, see: https://gitlab.com/cznic/sqlite/-/issues/207 return a.reg(p, size), nil } // reg registers the freshly mapped size-byte region at p. func (a *Allocator) reg(p uintptr, size int) uintptr { a.addLive(size) if counters { a.Mmaps++ a.Bytes += size } if a.regs == nil { a.regs = map[uintptr]struct{}{} } (*page)(unsafe.Pointer(p)).size = size a.regs[p] = struct{}{} return p } func (a *Allocator) newPage(size int) (uintptr /* *page */, error) { size += int(headerSize) p, err := a.mmap(size) if err != nil { return 0, err } (*page)(unsafe.Pointer(p)).log = 0 return p, nil } func (a *Allocator) newSharedPage(log uint) (uintptr /* *page */, error) { if a.cap[log] == 0 { a.cap[log] = int(pageAvail) / (1 << log) } size := int(headerSize) + a.cap[log]< hot { if a.cold == nil { a.cold = map[int]int{} } decommit(s[a.cold[size]], size) a.cold[size]++ } } // UintptrCalloc is like Calloc except it returns an uintptr. func (a *Allocator) UintptrCalloc(size int) (r uintptr, err error) { if trace { defer func() { fmt.Fprintf(os.Stderr, "Calloc(%#x) %#x, %v\n", size, r, err) }() } if r, err = a.UintptrMalloc(size); r == 0 || err != nil { return 0, err } b := ((*rawmem)(unsafe.Pointer(r)))[:size:size] for i := range b { b[i] = 0 } return r, nil } // UintptrFree is like Free except its argument is an uintptr, which must have // been acquired from UintptrCalloc or UintptrMalloc or UintptrRealloc. func (a *Allocator) UintptrFree(p uintptr) (err error) { if trace { defer func() { fmt.Fprintf(os.Stderr, "Free(%#x) %v\n", p, err) }() } if p == 0 { return nil } if counters { a.Allocs-- } pg := p &^ uintptr(pageMask) log := (*page)(unsafe.Pointer(pg)).log if log == 0 { return a.release(pg) } (*node)(unsafe.Pointer(p)).prev = 0 (*node)(unsafe.Pointer(p)).next = a.lists[log] if next := (*node)(unsafe.Pointer(p)).next; next != 0 { (*node)(unsafe.Pointer(next)).prev = p } a.lists[log] = p (*page)(unsafe.Pointer(pg)).used-- if (*page)(unsafe.Pointer(pg)).used != 0 { return nil } for i := 0; i < (*page)(unsafe.Pointer(pg)).brk; i++ { n := pg + headerSize + uintptr(i)< int(^uint(0)>>1)-int(headerSize) { return 0, fmt.Errorf("memory: allocation size %d is too large", size) } if counters { a.Allocs++ } log := uint(bits.Len(uint((size+int(mallocAllign)-1)&^int(mallocAllign-1) - 1))) if log > maxSlotSizeLog { p, err := a.newPage(size) if err != nil { return 0, err } return p + headerSize, nil } if a.lists[log] == 0 && a.pages[log] == 0 { if _, err := a.newSharedPage(log); err != nil { return 0, err } } if p := a.pages[log]; p != 0 { (*page)(unsafe.Pointer(p)).used++ (*page)(unsafe.Pointer(p)).brk++ if (*page)(unsafe.Pointer(p)).brk == a.cap[log] { a.pages[log] = 0 } return p + headerSize + uintptr((*page)(unsafe.Pointer(p)).brk-1)<= size { return p, nil } if r, err = a.UintptrMalloc(size); err != nil { return 0, err } if us < size { size = us } copy((*rawmem)(unsafe.Pointer(r))[:size:size], (*rawmem)(unsafe.Pointer(p))[:size:size]) return r, a.UintptrFree(p) } // UintptrUsableSize is like UsableSize except its argument is an uintptr, // which must have been returned from UintptrCalloc, UintptrMalloc or // UintptrRealloc. func UintptrUsableSize(p uintptr) (r int) { if trace { defer func() { fmt.Fprintf(os.Stderr, "UsableSize(%#x) %#x\n", p, r) }() } if p == 0 { return 0 } return usableSize(p) } func usableSize(p uintptr) (r int) { pg := p &^ uintptr(pageMask) if log := (*page)(unsafe.Pointer(pg)).log; log != 0 { return 1 << log } return (*page)(unsafe.Pointer(pg)).size - int(headerSize) } // Calloc is like Malloc except the allocated memory is zeroed. func (a *Allocator) Calloc(size int) (r []byte, err error) { p, err := a.UintptrCalloc(size) if err != nil { return nil, err } return (*rawmem)(unsafe.Pointer(p))[:size:usableSize(p)], nil } // Close releases all OS resources used by a and sets it to its zero value. // // It's not necessary to Close the Allocator when exiting a process. func (a *Allocator) Close() (err error) { for p := range a.regs { if e := a.unmap(p); e != nil && err == nil { err = e } } *a = Allocator{} return err } // Free deallocates memory (as in C.free). The argument of Free must have been // acquired from Calloc or Malloc or Realloc. func (a *Allocator) Free(b []byte) (err error) { if b = b[:cap(b)]; len(b) == 0 { return nil } return a.UintptrFree(uintptr(unsafe.Pointer(&b[0]))) } // Malloc allocates size bytes and returns a byte slice of the allocated // memory. The memory is not initialized. Malloc panics for size < 0 and // returns (nil, nil) for zero size. // // It's ok to reslice the returned slice but the result of appending to it // cannot be passed to Free or Realloc as it may refer to a different backing // array afterwards. func (a *Allocator) Malloc(size int) (r []byte, err error) { p, err := a.UintptrMalloc(size) if p == 0 || err != nil { return nil, err } return (*rawmem)(unsafe.Pointer(p))[:size:usableSize(p)], nil } // Realloc changes the size of the backing array of b to size bytes or returns // an error, if any. The contents will be unchanged in the range from the // start of the region up to the minimum of the old and new sizes. If the // new size is larger than the old size, the added memory will not be // initialized. If b's backing array is of zero size, then the call is // equivalent to Malloc(size), for all values of size; if size is equal to // zero, and b's backing array is not of zero size, then the call is equivalent // to Free(b). Unless b's backing array is of zero size, it must have been // returned by an earlier call to Malloc, Calloc or Realloc. If the area // pointed to was moved, a Free(b) is done. func (a *Allocator) Realloc(b []byte, size int) (r []byte, err error) { var p uintptr if b = b[:cap(b)]; len(b) != 0 { p = uintptr(unsafe.Pointer(&b[0])) } if p, err = a.UintptrRealloc(p, size); p == 0 || err != nil { return nil, err } return (*rawmem)(unsafe.Pointer(p))[:size:usableSize(p)], nil } // Trim returns to the OS the empty regions a retains for reuse: each // size class's empty carve-target page and the bounded pool of retired // regions. Pages still having live allocations are not affected and a // stays ready for use. // // Trim is never necessary for correctness. It trades a smaller resident set // now for the cost of mapping those pages again later. func (a *Allocator) Trim() (err error) { if trace { defer func() { fmt.Fprintf(os.Stderr, "Trim() %v\n", err) }() } for log := range a.pages { pg := a.pages[log] if pg == 0 || (*page)(unsafe.Pointer(pg)).used != 0 { continue } a.pages[log] = 0 a.live -= (*page)(unsafe.Pointer(pg)).size if e := a.unmap(pg); e != nil && err == nil { err = e } } for _, s := range a.freed { for _, pg := range s { if e := a.unmap(pg); e != nil && err == nil { err = e } } } a.freed = nil a.freedSize = 0 a.cold = nil return err } // UsableSize reports the size of the memory block allocated at p, which must // point to the first byte of a slice returned from Calloc, Malloc or Realloc. // The allocated memory block size can be larger than the size originally // requested from Calloc, Malloc or Realloc. func UsableSize(p *byte) (r int) { return UintptrUsableSize(uintptr(unsafe.Pointer(p))) } // UnsafeCalloc is like Calloc except it returns an unsafe.Pointer. func (a *Allocator) UnsafeCalloc(size int) (r unsafe.Pointer, err error) { p, err := a.UintptrCalloc(size) if err != nil { return nil, err } return unsafe.Pointer(p), nil } // UnsafeFree is like Free except its argument is an unsafe.Pointer, which must // have been acquired from UnsafeCalloc or UnsafeMalloc or UnsafeRealloc. func (a *Allocator) UnsafeFree(p unsafe.Pointer) (err error) { return a.UintptrFree(uintptr(p)) } // UnsafeMalloc is like Malloc except it returns an unsafe.Pointer. func (a *Allocator) UnsafeMalloc(size int) (r unsafe.Pointer, err error) { p, err := a.UintptrMalloc(size) if err != nil { return nil, err } return unsafe.Pointer(p), nil } // UnsafeRealloc is like Realloc except its first argument is an // unsafe.Pointer, which must have been returned from UnsafeCalloc, // UnsafeMalloc or UnsafeRealloc. func (a *Allocator) UnsafeRealloc(p unsafe.Pointer, size int) (r unsafe.Pointer, err error) { q, err := a.UintptrRealloc(uintptr(p), size) if err != nil { return nil, err } return unsafe.Pointer(q), nil } // UnsafeUsableSize is like UsableSize except its argument is an // unsafe.Pointer, which must have been returned from UnsafeCalloc, // UnsafeMalloc or UnsafeRealloc. func UnsafeUsableSize(p unsafe.Pointer) (r int) { return UintptrUsableSize(uintptr(p)) }