rollinghash

package module
v4.2.0 Latest Latest
Warning

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

Go to latest
Published: Jun 30, 2026 License: MIT Imports: 3 Imported by: 0

README

CI Coverage Status GoDoc Reference

Rolling Hashes

Philosophy

This package provides several rolling hashes. The API design philosophy is to provide interfaces that are correct, fast and idiomatic. The hashes are drop-in replacements whenever a builtin counterpart exists.

Usage

Roller

rollinghash.Hash is the simplest interface: call Roll once per incoming byte and read the updated hash immediately. It is the right choice when the data is already in memory or when throughput is not the bottleneck.

data := []byte("here is some data to roll on")
h := buzhash64.New()
n := 16 // window size

h.Write(data[:n])

for _, c := range data[n:] {
    h.Roll(c)
    fmt.Println(h.Sum64())
}

The hash maintains an internal copy of the rolling window. Use WriteWindow to read it back out.

BatchRoller

rollinghash.BatchRoller is designed for searching a block within a stream, rsync-style: the rolling checksum acts as a cheap filter, and a secondary check (e.g. byte comparison) confirms the match. It batches computations to exploit instruction-level parallelism, achieving about twice the throughput of Roll.

data := []byte("the quick brown fox jumps over the lazy dog")

needle := []byte("brown")
window := len(needle)

h := buzhash64.New()
h.Write(needle)
target := h.Sum64()

s := rollinghash.NewBatchRoller(bytes.NewReader(data), buzhash64.New(), window)
for s.Next() {
    sums, buf := s.Sums(), s.Bytes()
    for i, sum := range sums {
        if sum == target && bytes.Equal(buf[i:i+window], needle) {
            fmt.Printf("found %q at offset %d\n", needle, s.Offset()+i)
        }
    }
}
if err := s.Err(); err != nil {
    log.Fatal(err)
}

Within each batch, Sums()[i] is the checksum of Bytes()[i:i+window], at stream position Offset()+i. Use WithBufferSize to control the batch size and Reset to reuse the batch roller across multiple streams without extra allocations.

Chunker

rollinghash.Chunker is designed for Content Defined Chunking (CDC). It also operates on a stream, uses the same batch optimization as the BatchRoller, and therefore performs about as well. The stream is split wherever the rolling checksum matches a mask. Use WithBoundaries to keep chunk sizes within a desired range.

// Generate 4KiB of pseudo-random data
data := make([]byte, 4096)
x := uint32(1)
for i := range data {
    x ^= x << 13; x ^= x >> 17; x ^= x << 5
    data[i] = byte(x)
}

// Cut where the low 8 bits of the rolling checksum are zero,
// keeping each chunk between 64 and 1024 bytes.
c := rollinghash.NewChunker(bytes.NewReader(data), buzhash64.New(), 56, 0xff,
    rollinghash.WithBoundaries(64, 1024))

for c.Next() {
    chunk := c.Bytes()
    if c.ContentDefined() {
        fmt.Printf("boundary at %d: sum=0x%x\n", c.Offset()+len(chunk), c.Sum())
    } else {
        fmt.Printf("max cut at %d\n", c.Offset()+len(chunk))
    }
}
if err := c.Err(); err != nil {
    log.Fatal(err)
}

Use Reset to reuse the chunker across multiple streams without extra allocations.

Gotchas

Call Write before the first Roll

The rolling window MUST be initialized by calling Write first (which saves a copy). The byte leaving the rolling window is inferred from the internal copy of the rolling window, which is updated with every call to Roll.

Use concrete types for maximum speed

Do NOT cast the result of New() to rollinghash.Hash. The Go compiler cannot inline calls through an interface. This costs roughly 10% performance.

var h1 rollinghash.Hash
h1 = buzhash32.New()
h2 := buzhash32.New()

[...]

h1.Roll(b) // Not inlined (slow)
h2.Roll(b) // inlined (fast)
Buzhash CDC: avoid window sizes that are multiples of the word size

When using buzhash32 or buzhash64 for Content Defined Chunking, do NOT choose a window length that is a multiple of the word size (32 for buzhash32, 64 for buzhash64).

Buzhash (cyclic polynomial) rolls its sum by rotating the word one bit per byte, so the rotation wraps every word-size bytes. As a result, a run of identical bytes at least as long as the window collapses the hash to a single degenerate value (all-ones for odd multiples of the word size, zero for even multiples), losing all entropy. Such runs are extremely common in binary data (zero padding, 0xff flash padding, alignment), so on typical executables a 64-byte window makes buzhash64 return 0xffffffffffffffff about 1% of the time, badly skewing the low bits.

This is inherent to the cyclic polynomial construction and cannot be fixed by changing the byte table. Any window length that is not a multiple of the word size avoids it (e.g. use 48 or 56 instead of 64).

BatchRoller and Chunker bypass the hash's rolling window

BatchRoll and BatchBoundaries are bulk operations that do not update the hash's internal rolling window. After passing a hash to NewBatchRoller or NewChunker, calling h.WriteWindow() on that hash will not reflect the stream contents — its state is undefined. Use WindowSize() on the BatchRoller or Chunker instead.

Which hash to use

Benchmarked on 2026-06-30, linux/amd64, AMD Ryzen 7 PRO 7840U (go test -bench='BenchmarkChunker/.*/fused|BenchmarkBatchRoller/.*/1024KiB|BenchmarkRolling64B' -benchtime=3s -count=6 ./...):

Hash Roll (MB/s) Chunker (MB/s) BatchRoller (MB/s) Uniformly distributed Parametrizable
buzhash64 833 1491 1522 yes¹ yes
buzhash32 838 1453 1505 yes¹ yes
gearhash64 771 1476 1450 yes yes
bozo32 847 1139 1371 yes² yes (single multiplier)
bozo64 830 1131 1329 yes² yes (single multiplier)
rabinkarp64 508 780 847 yes yes
adler32 250 402 420 no³ no

¹ Provided the window size is not a multiple of the word size (32 for buzhash32, 64 for buzhash64). See Gotchas.

² For very small windows the output is bounded below 2⁶⁴ before modular wrapping kicks in, so high bits are biased. For bozo64 (multiplier a ≈ 2³²) wrapping begins at window size 3; for bozo32 (multiplier a ≈ 2¹⁶) at window size 5. Any practical CDC window size is well above these thresholds.

³ adler32 is not uniformly distributed for small windows: its two component sums are bounded by window × 255, so the high bits of the output are always zero. Do not use adler32 for CDC. It is only useful for rsync-style block matching where the peer already uses adler32 (e.g. the rsync protocol itself).

buzhash64 is the fastest overall and a solid default for both CDC and block search.

gearhash64 is the popular choice from the CDC literature (see the FastCDC paper). It is essentially as fast as buzhash on the BatchRoller, has no window-size gotcha, and is uniformly distributed.

bozo32/bozo64 are very fast and parametrizable via a single integer multiplier (NewFromInt), which is simpler than buzhash's 256-entry table but sufficient to produce independent hash functions.

rabinkarp64 is the slowest but lets you pick a specific irreducible polynomial, which matters when you need to match an existing implementation (e.g. restic).

License

This code is delivered to you under the terms of the MIT public license, except the rabinkarp64 subpackage, which has been adapted from restic (BSD 2-clause "Simplified").

Notable users

This library is used by a wide variety of tools, for production and scientific purposes.

If you are using successfully, let me know and I will happily put a link here!

Documentation

Overview

Package rollinghash implements rolling versions of some hashes

Index

Examples

Constants

View Source
const DefaultWindowCap = 64

DefaultWindowCap is the default capacity of the internal window of a new Hash.

Variables

This section is empty.

Functions

func WithBoundaries added in v4.2.0

func WithBoundaries(min, max int) chunkerOption

WithBoundaries sets the minimum and maximum chunk size. Chunks shorter than min bytes are extended to the next boundary; chunks that reach max bytes without a mask hit are cut there unconditionally. Defaults are 0 and math.MaxInt.

func WithBufferSize added in v4.2.0

func WithBufferSize(n int) batchRollerOption

WithBufferSize sets the internal batch buffer size in bytes. A larger buffer means larger batches and better amortization of the bulk fast path; it must be at least window bytes. The default is 64 KiB.

Example

Buffer controls the batch size used by the BatchRoller. A larger buffer means fewer Next() calls and better amortization of the bulk fast path, at the cost of higher memory use. The default buffer is 64 KiB; here we use a small buffer to show that the BatchRoller produces correct results regardless of how the input is split across batches.

package main

import (
	"bytes"
	"fmt"
	"log"

	rollinghash "github.com/chmduquesne/rollinghash/v4"
	"github.com/chmduquesne/rollinghash/v4/buzhash64"
)

func main() {
	data := []byte("the quick brown fox jumps over the lazy dog")

	needle := []byte("brown")
	window := len(needle)

	h := buzhash64.New()
	if _, err := h.Write(needle); err != nil {
		log.Fatal(err)
	}
	target := h.Sum64()

	// Use the smallest valid buffer (window bytes) so every Next() call
	// returns exactly one position, exercising the batch-boundary logic.
	s := rollinghash.NewBatchRoller(bytes.NewReader(data), buzhash64.New(), window, rollinghash.WithBufferSize(window))

	for s.Next() {
		sums, buf := s.Sums(), s.Bytes()
		for i, sum := range sums {
			if sum == target && bytes.Equal(buf[i:i+window], needle) {
				fmt.Printf("found %q at offset %d\n", needle, s.Offset()+i)
			}
		}
	}
	if err := s.Err(); err != nil {
		log.Fatal(err)
	}
}
Output:
found "brown" at offset 10

Types

type BatchRoller added in v4.2.0

type BatchRoller interface {
	Next() bool
	Bytes() []byte
	Sums() []uint64
	Offset() int
	WindowSize() int
	Err() error
	Reset(r io.Reader)
}

BatchRoller walks an io.Reader and yields, per batch, the rolling checksum at every window position together with the bytes those checksums cover. The underlying hash must implement BatchRoll.

s := NewBatchRoller(r, h, window)
for s.Next() {
	sums, data := s.Sums(), s.Bytes()
	for i, sum := range sums {
		// sum == rolling checksum of data[i:i+window]
	}
}
if err := s.Err(); err != nil { ... }

Per-batch guarantee: len(Sums()) == len(Bytes())-window+1, and Sums()[i] is the checksum of Bytes()[i:i+window]. Consecutive batches overlap by window-1 bytes so no window position is skipped or duplicated.

Sums and Bytes are valid only until the next call to Next. Offset returns the stream position of Bytes()[0]; Sums()[i] is at Offset()+i. WindowSize returns the rolling window size; Sums()[i] covers Bytes()[i:i+WindowSize()]. Use WithBufferSize to control the batch size in bytes (default 64 KiB). Reset reuses internal allocations across streams.

Note: BatchRoll bypasses the hash's internal rolling window. After passing h to NewBatchRoller, h.WriteWindow() will not reflect the stream — use WindowSize() on the BatchRoller instead.

Example

BatchRoller is designed to support users who want to search for a given block within a stream, rsync-style. The rolling checksum acts as a cheap filter, and another method (e.g. byte comparison) confirms the match. Because it batches computations, it can exploit instruction-level parallelism, achieving roughly twice the throughput of Roll().

package main

import (
	"bytes"
	"fmt"
	"log"

	rollinghash "github.com/chmduquesne/rollinghash/v4"
	"github.com/chmduquesne/rollinghash/v4/buzhash64"
)

func main() {
	data := []byte("the quick brown fox jumps over the lazy dog")

	// The block we are looking for, and its rolling checksum.
	needle := []byte("brown")
	window := len(needle)

	h := buzhash64.New()
	if _, err := h.Write(needle); err != nil {
		log.Fatal(err)
	}
	target := h.Sum64()

	// Roll the stream. Within each batch, Sums()[i] is the checksum of
	// Bytes()[i:i+window], at stream position Offset()+i.
	s := rollinghash.NewBatchRoller(bytes.NewReader(data), buzhash64.New(), window)
	for s.Next() {
		sums, buf := s.Sums(), s.Bytes()
		for i, sum := range sums {
			if sum == target && bytes.Equal(buf[i:i+window], needle) {
				fmt.Printf("found %q at offset %d\n", needle, s.Offset()+i)
			}
		}
	}
	if err := s.Err(); err != nil {
		log.Fatal(err)
	}
}
Output:
found "brown" at offset 10

func NewBatchRoller added in v4.2.0

func NewBatchRoller(r io.Reader, h Hash, window int, opts ...batchRollerOption) BatchRoller

NewBatchRoller returns a BatchRoller over r. window must be >= 1. h must implement BatchRoll; NewBatchRoller panics otherwise. Pass nil for r and call Reset before the first Next to defer stream attachment. Use WithBufferSize to control the batch size (default 64 KiB).

type Chunker added in v4.2.0

type Chunker interface {
	Next() bool
	Bytes() []byte
	ContentDefined() bool
	Sum() uint64
	Offset() int
	WindowSize() int
	Err() error
	Reset(r io.Reader)
}

Chunker splits an io.Reader into content-defined chunks. The underlying hash must implement BatchBoundaries.

c := NewChunker(r, h, window, mask)
for c.Next() {
	chunk := c.Bytes()
	if c.ContentDefined() {
		// content-defined boundary; Sum() is the hit value
	} else {
		// forced cut at max, or the final chunk
	}
}
if err := c.Err(); err != nil { ... }

Bytes is valid only until the next call to Next. ContentDefined reports whether the current chunk ended at a mask hit (true) or was forced at max / end of stream (false). Sum returns the rolling checksum at a mask boundary; it returns 0 on forced cuts. Offset returns the start byte offset of the current chunk in the stream; the end offset is Offset()+len(Bytes()). WindowSize returns the rolling window size used for boundary detection. Use WithBoundaries to set minimum and maximum chunk sizes (defaults: 0 and math.MaxInt). Reset reuses internal allocations across streams.

Note: BatchBoundaries bypasses the hash's internal rolling window. After passing h to NewChunker, h.WriteWindow() will not reflect the stream — use WindowSize() on the Chunker instead.

Example

The Chunker interface was designed to support users who want to use rolling hashes for Content Defined Chunking (CDC). It also operates on a stream, which allows for batch computation optimizations similar to the ones used with the BatchRoller. In this type of situation, the stream is split where the rolling checksum hits a mask. Use WithBoundaries to keep chunk sizes within a desired range.

package main

import (
	"bytes"
	"fmt"
	"log"

	rollinghash "github.com/chmduquesne/rollinghash/v4"
	"github.com/chmduquesne/rollinghash/v4/buzhash64"
)

func main() {
	// Repeatable pseudo-random data (xorshift), so the boundaries are stable.
	data := make([]byte, 4096)
	x := uint32(1)
	for i := range data {
		x ^= x << 13
		x ^= x >> 17
		x ^= x << 5
		data[i] = byte(x)
	}

	// Cut where the low 8 bits of the rolling checksum are zero, keeping each
	// chunk between 64 and 1024 bytes.
	c := rollinghash.NewChunker(bytes.NewReader(data), buzhash64.New(), 56, 0xff, rollinghash.WithBoundaries(64, 1024))

	var sizes []int
	total := 0
	for c.Next() {
		chunk := c.Bytes()
		sizes = append(sizes, len(chunk))
		total += len(chunk)
		if c.ContentDefined() {
			fmt.Printf("boundary at %d: sum=0x%x\n", total, c.Sum())
		} else {
			fmt.Printf("max cut   at %d\n", total)
		}
	}
	if err := c.Err(); err != nil {
		log.Fatal(err)
	}

	fmt.Printf("split %d bytes into %d chunks: %v\n", total, len(sizes), sizes)
}
Output:
boundary at 123: sum=0xbd9f33d05f52e700
boundary at 277: sum=0x3a611bc3e53cf900
boundary at 651: sum=0xecb29647a13a3600
boundary at 769: sum=0x3109e14cbfa7da00
boundary at 1436: sum=0xb61cdeda53dac000
boundary at 1522: sum=0x8bef143657fed400
boundary at 1722: sum=0x35f525a03a01d000
boundary at 2173: sum=0xb168bf8f4418ee00
boundary at 2404: sum=0x6c13f4fb45436f00
boundary at 2647: sum=0x33695e700dcdf300
boundary at 3388: sum=0xe915cd64f38a9800
boundary at 3837: sum=0xdfc83351b3d06800
max cut   at 4096
split 4096 bytes into 13 chunks: [123 154 374 118 667 86 200 451 231 243 741 449 259]

func NewChunker added in v4.2.0

func NewChunker(r io.Reader, h Hash, window int, mask uint64, opts ...chunkerOption) Chunker

NewChunker returns a chunker over r. A boundary is placed where the rolling checksum under h (over window bytes) satisfies checksum & mask == 0, with the chunk length kept in [min, max]. window must be >= 1. Use WithMinSize and WithMaxSize to set min (default 0) and max (default math.MaxInt). The hash must implement BatchBoundaries; NewChunker panics otherwise.

type Hash

type Hash interface {
	hash.Hash
	Roller
}

rollinghash.Hash extends hash.Hash by adding the method Roll. A rollinghash.Hash can be updated byte by byte, by specifying which byte enters the window. A rollinghash.Hash internally maintains a copy of the rolling window in order to keep track of the value of the byte exiting the window. This copy is updated with every call to Roll. The rolling window can be accessed through the io.Reader interface.

type Hash32

type Hash32 interface {
	hash.Hash32
	Roller
}

rollinghash.Hash32 extends hash.Hash by adding the method Roll. A rollinghash.Hash32 can be updated byte by byte, by specifying which byte enters the window. A rollinghash.Hash32 internally maintains a copy of the rolling window in order to keep track of the value of the byte exiting the window. This copy is updated with every call to Roll. The rolling window can be accessed through the io.Reader interface.

type Hash64

type Hash64 interface {
	hash.Hash64
	Roller
}

rollinghash.Hash64 extends hash.Hash by adding the method Roll. A rollinghash.Hash64 can be updated byte by byte, by specifying which byte enters the window. A rollinghash.Hash64 internally maintains a copy of the rolling window in order to keep track of the value of the byte exiting the window. This copy is updated with every call to Roll. The rolling window can be accessed through the io.Reader interface.

type Roller

type Roller interface {
	Roll(b byte)

	// WriteWindow writes the contents of the current window to w.
	//
	// It returns the number of bytes written and any error returned by
	// w.Write.
	WriteWindow(w io.Writer) (int, error)
}

A Roller is a type that has the method Roll. Roll updates the hash of a rolling window from just the entering byte. You MUST call Write() BEFORE using this method and provide it with an initial window of size at least 1 byte. You can then call this method for every new byte entering the window. The byte leaving the window is automatically computed from a copy of the window internally kept in the checksum. This window is updated along with the internal state of the checksum every time Roll() is called.

Directories

Path Synopsis
Package rollinghash/gearhash64 implements the Gear rolling hash (64-bit).
Package rollinghash/gearhash64 implements the Gear rolling hash (64-bit).
internal
window
Package window provides common code for dealing with sliding windows.
Package window provides common code for dealing with sliding windows.

Jump to

Keyboard shortcuts

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