Documentation ¶
Overview ¶
Package bufs implements a simple buffer cache.
The intended use scheme is like:
type Foo struct { buffers bufs.Buffers ... } // Bar can call Qux, but not the other way around (in this example). const maxFooDepth = 2 func NewFoo() *Foo { return &Foo{buffers: bufs.New(maxFooDepth), ...} } func (f *Foo) Bar(n int) { buf := f.buffers.Alloc(n) // needed locally for computation and/or I/O defer f.buffers.Free() ... f.Qux(whatever) } func (f *Foo) Qux(n int) { buf := f.buffers.Alloc(n) // needed locally for computation and/or I/O defer f.buffers.Free() ... }
The whole idea behind 'bufs' is that when calling e.g. Foo.Bar N times, then normally, without using 'bufs', there will be 2*N (in this example) []byte buffers allocated. While using 'bufs', only 2 buffers (in this example) will ever be created. For large N it can be a substantial difference.
It's not a good idea to use Buffers to cache too big buffers. The cost of having a cached buffer is that the buffer is naturally not eligible for garbage collection. Of course, that holds only while the Foo instance is reachable, in the above example.
The buffer count limit is intentionally "hard" (read panicking), although configurable in New(). The rationale is to prevent recursive calls, using Alloc, to cause excessive, "static" memory consumption. Tune the limit carefully or do not use Buffers from within [mutually] recursive functions where the nesting depth is not realistically bounded to some rather small number.
Buffers cannot guarantee improvements to you program performance. There may be a gain in case where they fit well. Firm grasp on what your code is actually doing, when and in what order is essential to proper use of Buffers. It's _highly_ recommended to first do profiling and memory profiling before even thinking about using 'bufs'. The real world example, and cause for this package, was a first correct, yet no optimizations done version of a program; producing few MB of useful data while allocating 20+GB of memory. Of course the garbage collector properly kicked in, yet the memory abuse caused ~80+% of run time to be spent memory management. The program _was_ expected to be slow in its still development phase, but the bottleneck was guessed to be in I/O. Actually the hard disk was waiting for the billions bytes being allocated and zeroed. Garbage collect on low memory, rinse and repeat.
In the provided tests, TestFoo and TestFooBufs do the same simulated work, except the later uses Buffers while the former does not. Suggested test runs which show the differences:
$ go test -bench . -benchmem or $ go test -c $ ./bufs.test -test.v -test.run Foo -test.memprofile mem.out -test.memprofilerate 1 $ go tool pprof bufs.test mem.out --alloc_space --nodefraction 0.0001 --edgefraction 0 -web $ # Note: Foo vs FooBufs allocated memory is in hundreds of MBs vs 8 kB. or $ make demo # same as all of the above
NOTE: Alloc/Free calls must be properly nested in the same way as in for example BeginTransaction/EndTransaction pairs. If your code can panic then the pairing should be enforced by deferred calls.
NOTE: Buffers objects do not allocate any space until requested by Alloc, the mechanism works on demand only.
FAQ: Why the 'bufs' package name?
Package name 'bufs' was intentionally chosen instead of the perhaps more conventional 'buf'. There are already too many 'buf' named things in the code out there and that'll be a source of a lot of trouble. It's a bit similar situation as in the case of package "strings" (not "string").
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Buffers ¶
type Buffers [][]byte
Buffers type represents a buffer ([]byte) cache.
NOTE: Do not modify Buffers directly, use only its methods. Do not create additional values (copies) of Buffers, that'll break its functionality. Use a pointer instead to refer to a single instance from different places/scopes.
func New ¶
New returns a newly created instance of Buffers with a maximum capacity of n buffers.
NOTE: 'bufs.New(n)' is the same as 'make(bufs.Buffers, n)'.
func (*Buffers) Alloc ¶
Alloc will return a buffer such that len(r) == n. It will firstly try to find an existing and unused buffer of big enough size. Only when there is no such, then one of the buffer slots is reallocated to a bigger size.
It's okay to use append with buffers returned by Alloc. But it can cause allocation in that case and will again be producing load for the garbage collector. The best use of Alloc is for I/O buffers where the needed size of the buffer is figured out at some point of the code path in a 'final size' sense. Another real world example are compression/decompression buffers.
NOTE: The buffer returned by Alloc _is not_ zeroed. That's okay for e.g. passing a buffer to io.Reader. If you need a zeroed buffer use Calloc.
NOTE: Buffers returned from Alloc _must not_ be exposed/returned to your clients. Those buffers are intended to be used strictly internally, within the methods of some "object".
NOTE: Alloc will panic if there are no buffers (buffer slots) left.
func (*Buffers) Calloc ¶
Calloc will acquire a buffer using Alloc and then clears it to zeros. The zeroing goes up to n, not cap(r).
type CCache ¶
type CCache struct {
// contains filtered or unexported fields
}
CCache is a Cache which is safe for concurrent use by multiple goroutines.
var GCache CCache
GCache is a ready to use global instance of a CCache.
func (*CCache) Cget ¶
Cget will acquire a buffer using Get and then clears it to zeros. The zeroing goes up to n, not cap(r).
func (*CCache) Get ¶
Get returns a buffer ([]byte) of length n. If no such buffer is cached then a biggest cached buffer is resized to have length n and returned. If there are no cached items at all, Get returns a newly allocated buffer.
In other words the cache policy is:
- If the cache is empty, the buffer must be newly created and returned. Cache remains empty.
- If a buffer of sufficient size is found in the cache, remove it from the cache and return it.
- Otherwise the cache is non empty, but no cached buffer is big enough. Enlarge the biggest cached buffer, remove it from the cache and return it. This provide cached buffers size adjustment based on demand.
In short, if the cache is not empty, Get guarantees to make it always one item less. This rules prevent uncontrolled cache grow in some scenarios. The older policy was not preventing that. Another advantage is better cached buffers sizes "auto tuning", although not in every possible use case.
NOTE: The buffer returned by Get _is not guaranteed_ to be zeroed. That's okay for e.g. passing a buffer to io.Reader. If you need a zeroed buffer use Cget.
type Cache ¶
type Cache [][]byte
Cache caches buffers ([]byte). A zero value of Cache is ready for use.
NOTE: Do not modify a Cache directly, use only its methods. Do not create additional values (copies) of a Cache, that'll break its functionality. Use a pointer instead to refer to a single instance from different places/scopes.
func (*Cache) Cget ¶
Cget will acquire a buffer using Get and then clears it to zeros. The zeroing goes up to n, not cap(r).
func (*Cache) Get ¶
Get returns a buffer ([]byte) of length n. If no such buffer is cached then a biggest cached buffer is resized to have length n and returned. If there are no cached items at all, Get returns a newly allocated buffer.
In other words the cache policy is:
- If the cache is empty, the buffer must be newly created and returned. Cache remains empty.
- If a buffer of sufficient size is found in the cache, remove it from the cache and return it.
- Otherwise the cache is non empty, but no cached buffer is big enough. Enlarge the biggest cached buffer, remove it from the cache and return it. This provide cached buffers size adjustment based on demand.
In short, if the cache is not empty, Get guarantees to make it always one item less. This rules prevent uncontrolled cache grow in some scenarios. The older policy was not preventing that. Another advantage is better cached buffers sizes "auto tuning", although not in every possible use case.
NOTE: The buffer returned by Get _is not guaranteed_ to be zeroed. That's okay for e.g. passing a buffer to io.Reader. If you need a zeroed buffer use Cget.