buffer

package
v0.0.28 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 31, 2026 License: Apache-2.0 Imports: 3 Imported by: 0

Documentation

Overview

Package buffer provides the byte buffer that columns store their values in.

A Buffer is a byte slice with two properties an ordinary slice does not have. Its first byte sits at an address that is a multiple of 64, and its capacity is rounded up to a multiple of 64.

Both of those are for the kernels. An aligned load is the cheaper instruction on every vector unit that offers the choice, and an aligned start means a run over a column does not straddle a cache line at every boundary for no reason. The rounded up capacity is the more valuable half: it means a kernel handling the last few elements of a column can issue a full width load that reads past the end of the data, because the bytes it reads are inside the allocation and belong to nobody else. Without that, every kernel needs a scalar tail loop, and the tail loop is where the bugs are.

Go's allocator promises 8 byte alignment and nothing stronger, so the portable way to get more is to ask for 63 bytes extra and start at the first aligned address inside them. That turned out to cost far more than 63 bytes, because the extra request lands in the next allocator size class: measured, 19 percent on a 4 kilobyte buffer and 12.5 percent on a 64 kilobyte one. So the buffer asks for exactly what it wants first and checks where it landed, which in practice is on a boundary every time, and only pays for the padding when it is not. The check costs one comparison and the fallback is still there, because none of what makes the check succeed is anything Go promises.

There is no reference counting. Buffers are ordinary Go memory and the garbage collector already knows how to free them. Arrow implementations in other languages need Retain and Release because their host languages have no collector, and porting that discipline to Go would be a tax that everybody forgets to pay exactly once, in the place it matters. Pool is the escape hatch for the executor, where a scratch buffer has a lifetime short enough to prove by reading the code.

The zero Buffer is empty and ready to use.

Stability: tier 1, stable.

Index

Examples

Constants

View Source
const Alignment = 64

Alignment is the address boundary every buffer starts on, and the multiple every capacity is rounded up to.

64 is the AVX-512 vector width, and also the cache line size on every x86 part since the Pentium 4 and on arm64. Arrow recommends the same number for the same reasons, so a buffer that satisfies this also satisfies anything that wants to read it over the C data interface.

Variables

This section is empty.

Functions

This section is empty.

Types

type Buffer

type Buffer struct {
	// contains filtered or unexported fields
}

Buffer is a growable, aligned sequence of bytes.

func New

func New(n int) *Buffer

New returns a buffer of n zeroed bytes.

Example
package main

import (
	"fmt"

	"github.com/tamnd/kuma/buffer"
)

func main() {
	b := buffer.New(8)
	copy(b.Bytes(), "kuma")

	fmt.Println(b.Len(), b.Cap(), b.Aligned())
	fmt.Printf("%q\n", b.Bytes())
}
Output:
8 64 true
"kuma\x00\x00\x00\x00"

func Wrap

func Wrap(p []byte) *Buffer

Wrap returns a buffer backed by p, without copying it, and takes ownership of it. The caller must not use p afterwards.

This is the path for bytes that came from somewhere that already laid them out, meaning a memory mapped file or an Arrow IPC message. Such bytes are usually aligned, because the format asks for it, but nothing here checks and Aligned is how a caller finds out. A wrapped buffer that has to grow stops being wrapped, since growing means a fresh aligned allocation and a copy.

func (*Buffer) Aligned

func (b *Buffer) Aligned() bool

Aligned reports whether the buffer starts on an Alignment boundary. It is true for every buffer this package allocates, and it is worth asking about a wrapped one.

func (*Buffer) Append

func (b *Buffer) Append(p []byte)

Append adds p to the end.

Example
package main

import (
	"fmt"

	"github.com/tamnd/kuma/buffer"
)

func main() {
	var b buffer.Buffer
	b.Append([]byte("hello "))
	b.Append([]byte("world"))

	fmt.Println(string(b.Bytes()))
}
Output:
hello world

func (*Buffer) Bytes

func (b *Buffer) Bytes() []byte

Bytes returns the bytes in use. Modifying the result modifies the buffer.

The result stops at the length, so the padding past it is not visible here. A kernel that wants to read the padding on purpose, which is the reason the padding exists, has to go through Padded.

func (*Buffer) Cap

func (b *Buffer) Cap() int

Cap returns how many bytes the buffer holds before it has to grow.

func (*Buffer) Clone

func (b *Buffer) Clone() *Buffer

Clone returns a copy that shares no memory with b. The copy is aligned even if b was wrapped around something that was not.

func (*Buffer) Grow

func (b *Buffer) Grow(n int)

Grow makes room for n more bytes without adding them, reallocating if it has to. It is worth calling when the final size is known.

func (*Buffer) Len

func (b *Buffer) Len() int

Len returns the number of bytes in use.

func (*Buffer) Padded

func (b *Buffer) Padded() []byte

Padded returns the bytes in use followed by the padding after them, which is the whole allocation. It is what a kernel reads when it wants to process the final partial vector without a scalar tail loop.

The padding bytes are not part of the data and their contents mean nothing. A kernel may read them and must not let them change its answer.

The result is a whole number of Alignment sized blocks for every buffer this package allocates. It is not for a wrapped one, which is exactly as long as the bytes it was handed, so a kernel that reads whole vectors has to check Aligned or work on a Clone.

Example

ExampleBuffer_Padded shows the property the padding exists for. A buffer holding one byte of data still hands a kernel a full block to load, so the last elements of a column do not need a separate scalar loop.

package main

import (
	"fmt"

	"github.com/tamnd/kuma/buffer"
)

func main() {
	b := buffer.New(1)

	fmt.Println(len(b.Bytes()), len(b.Padded()))
}
Output:
1 64

func (*Buffer) Reset

func (b *Buffer) Reset()

Reset sets the length to zero and keeps the memory. The bytes that were there are still there until something writes over them, which is what makes this cheap and why Resize zeroes on the way back up.

func (*Buffer) Resize

func (b *Buffer) Resize(n int)

Resize sets the length to n. Growing zeroes the new bytes, so a buffer that was shrunk and then grown again never shows what used to be there.

Example
package main

import (
	"fmt"

	"github.com/tamnd/kuma/buffer"
)

func main() {
	b := buffer.New(4)
	copy(b.Bytes(), "kuma")

	b.Resize(2)
	fmt.Printf("%q\n", b.Bytes())

	// Growing back does not bring the old bytes with it.
	b.Resize(4)
	fmt.Printf("%q\n", b.Bytes())
}
Output:
"ku"
"ku\x00\x00"

func (*Buffer) Zero

func (b *Buffer) Zero()

Zero sets every byte to zero, including the padding past the length.

type Pool

type Pool struct {
	// contains filtered or unexported fields
}

Pool recycles scratch buffers.

The engine allocates and drops buffers in a very particular pattern: an operator takes one for the duration of a morsel, writes it, reads it, and is done with it before the next morsel starts. Those are all the same handful of sizes, they are all short lived, and there are a great many of them. That is the shape a free list is good at and the shape a garbage collector has to work hardest at, because every one of them survives long enough to be scanned and none of them survive long enough to be worth having been scanned.

This is the only place in kuma where memory has a lifetime that somebody has to reason about, and it is deliberately kept to the one place where the reasoning is easy. Use it when the buffer cannot outlive the function that asked for it and the code makes that obvious. Anywhere else, call New and let the collector do its job.

The zero Pool is empty and ready to use. It is safe for concurrent use, and it must not be copied, which go vet enforces because the sync.Pool inside it must not be copied either. It is worth having one per executor rather than one per process, so that two queries running at once do not fight over the same free lists.

Example

ExamplePool is the shape an operator uses: take a buffer, use it, give it back before returning. The defer is what makes the lifetime obvious to somebody reading the code later, which is the condition for using a pool at all.

package main

import (
	"fmt"

	"github.com/tamnd/kuma/buffer"
)

func main() {
	var pool buffer.Pool

	scratch := pool.Get(1024)
	defer pool.Put(scratch)

	copy(scratch.Bytes(), "working space")
	fmt.Println(scratch.Len(), scratch.Cap())
}
Output:
1024 1024

func (*Pool) Get

func (p *Pool) Get(n int) *Buffer

Get returns a buffer of n bytes.

The bytes are whatever the previous user of that memory left in them. This is the difference between Get and New and it is the whole point: a scratch buffer that is about to be overwritten does not need to be zeroed first, and zeroing it would give back most of what the pool saves. Call Zero when the contents matter, or call New.

func (*Pool) Put

func (p *Pool) Put(b *Buffer)

Put offers a buffer back. The caller must not use it afterwards, and must not put the same buffer twice.

A buffer whose capacity is not one of the size classes is dropped rather than kept, because a free list of odd sizes is a free list nothing can be served from. That includes wrapped buffers, whose memory belongs to whatever handed it over. Dropping is not a failure and there is nothing to report: the buffer is ordinary Go memory and the collector takes it from here.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL