zig-skills/references/std-hash.md

13 KiB

std.hash - Hash Functions (Zig 0.16.0)

Non-cryptographic hash functions for hash tables, checksums, and data integrity. For cryptographic hashing, use std.crypto.hash.

Primary Zig 0.16 release-note source: https://ziglang.org/download/0.16.0/release-notes.html

Examples that hash file contents should use std.Io.Dir/std.Io.File and explicit std.Io for file access.

Quick Reference

Category Types
General Purpose Wyhash (default for HashMap), XxHash64, XxHash32, XxHash3
Classic Fnv1a_32, Fnv1a_64, Fnv1a_128, Murmur2_32, Murmur2_64, Murmur3_32
Checksum Crc32, Adler32, crc.* (100+ CRC variants)
City CityHash32, CityHash64
SipHash SipHash64, SipHash128 (from std.crypto.siphash)
Auto-hashing autoHash, autoHashStrat, hash.int

Choosing a Hash Function

Need a hash for HashMap?
├─ Yes → Use default (Wyhash via std.hash.autoHash)
└─ No → Need checksum/integrity?
        ├─ Yes → Crc32 or Adler32
        └─ No → Need speed?
               ├─ Yes → XxHash3, XxHash64, or Wyhash
               └─ No → Fnv1a (simple), Murmur (portable)
Hash Output Speed Use Case
Wyhash 64-bit Very fast Default for HashMap, general hashing
XxHash3 64-bit Fastest Large data, streaming
XxHash64 64-bit Fast General purpose
Fnv1a_64 64-bit Moderate Simple, small code size
Murmur3_32 32-bit Fast Portable, well-tested
CityHash64 64-bit Fast Google's hash, good distribution
Crc32 32-bit Fast Checksums, error detection
Adler32 32-bit Very fast Lightweight checksums

Basic Usage

One-Shot Hashing

const std = @import("std");
const hash = std.hash;

// Wyhash (recommended default)
const h1 = hash.Wyhash.hash(0, "hello world");

// XxHash
const h2 = hash.XxHash64.hash(0, "hello world");
const h3 = hash.XxHash3.hash(0, "hello world");

// FNV-1a
const h4 = hash.Fnv1a_64.hash("hello world");

// Murmur
const h5 = hash.Murmur2_64.hash("hello world");
const h6 = hash.Murmur3_32.hash("hello world");

// CityHash
const h7 = hash.CityHash64.hash("hello world");

Streaming/Incremental Hashing

Hashers that expose init/update/final support incremental input. Some checksum types have a simpler state shape, and some algorithms expose only one-shot helpers; check the selected type rather than assuming a uniform interface.

const std = @import("std");

// Initialize hasher
var hasher = std.hash.Wyhash.init(0);  // 0 is the seed

// Update with data incrementally
hasher.update("hello ");
hasher.update("world");

// Get final hash
const result = hasher.final();

With Seed

// Different seeds produce different hashes
const seed: u64 = 12345;

const h1 = std.hash.Wyhash.hash(seed, "data");
const h2 = std.hash.XxHash64.hash(seed, "data");

// Fnv1a doesn't take a seed in hash(), use init() for streaming
var fnv = std.hash.Fnv1a_64.init();
fnv.update("data");
const h3 = fnv.final();

Auto-Hashing (Generic Types)

std.hash.autoHash hashes eligible Zig value types. It intentionally rejects ambiguous categories such as slices unless the caller selects an explicit pointer strategy, and some type categories are unsupported.

const std = @import("std");

const Point = struct {
    x: i32,
    y: i32,
};

fn hashPoint(p: Point) u64 {
    var hasher = std.hash.Wyhash.init(0);
    std.hash.autoHash(&hasher, p);
    return hasher.final();
}

// Works with types accepted by autoHash; slices/pointers require deliberate handling.
fn hashAny(value: anytype) u64 {
    var hasher = std.hash.Wyhash.init(0);
    std.hash.autoHash(&hasher, value);
    return hasher.final();
}

// Usage
const h1 = hashAny(Point{ .x = 10, .y = 20 });
const h2 = hashAny(@as(u32, 42));
const h3 = hashAny(MyEnum.value);

Hash Strategy for Pointers

autoHashStrat controls how pointers are hashed:

const std = @import("std");
const Strategy = std.hash.Strategy;

var hasher = std.hash.Wyhash.init(0);
const data: []const u8 = "hello";

// Shallow slice hashing includes its pointer and length, not its contents.
std.hash.autoHashStrat(&hasher, data, .Shallow);

// Deep: follow pointer, hash contents (one level)
std.hash.autoHashStrat(&hasher, data, .Deep);

// DeepRecursive: follow all pointers recursively
std.hash.autoHashStrat(&hasher, data, .DeepRecursive);
Strategy Behavior
.Shallow Hash a one-item pointer's address; for a slice, hash pointer and length, not elements
.Deep Follow pointer one level, hash contents
.DeepRecursive Follow all pointers, hash all contents

Integer Hashing

std.hash.int provides optimized integer-to-integer hashing:

const std = @import("std");

// Hash integers directly (preserves type)
const h1: u32 = std.hash.int(@as(u32, 12345));
const h2: u64 = std.hash.int(@as(u64, 12345));
const h3: i32 = std.hash.int(@as(i32, -42));

// Useful for hash table probing
fn probe(key: u64, attempt: usize) u64 {
    return std.hash.int(key +% @as(u64, attempt));
}

Checksum Functions

CRC32

const std = @import("std");
const Crc32 = std.hash.Crc32;

// One-shot
const checksum = Crc32.hash("data to checksum");

// Streaming
var crc = Crc32.init();
crc.update("data ");
crc.update("to checksum");
const result = crc.final();

CRC Variants

Over 100 CRC variants available:

const crc = std.hash.crc;

// Common variants
const Crc32IsoHdlc = crc.Crc32IsoHdlc;  // Standard CRC-32 (default)
const Crc32Iscsi = crc.Crc32Iscsi;      // CRC-32C (Castagnoli)
const Crc16Usb = crc.Crc16Usb;
const Crc16Modbus = crc.Crc16Modbus;
const Crc8Bluetooth = crc.Crc8Bluetooth;

// Usage
const checksum = crc.Crc32Iscsi.hash("data");

Custom CRC

const std = @import("std");
const Crc = std.hash.crc.Crc;

// Define custom CRC
const MyCrc = Crc(u16, .{
    .polynomial = 0x8005,
    .initial = 0xFFFF,
    .reflect_input = true,
    .reflect_output = true,
    .xor_output = 0x0000,
});

const checksum = MyCrc.hash("data");

Adler32

Faster than CRC but weaker error detection:

const std = @import("std");
const Adler32 = std.hash.Adler32;

const checksum = Adler32.hash("data");

// Streaming
var adler: Adler32 = .{};
adler.update("data");
const result = adler.adler;

Using with HashMap

AutoHashMap uses the stdlib's automatic hashing context. StringHashMap uses a string-specific context, while custom HashMap users provide their own context:

const std = @import("std");

// Default string HashMap (uses Wyhash internally)
var map = std.StringHashMap(u32).init(allocator);
defer map.deinit();

try map.put("key", 42);

// Custom key type
const Point = struct {
    x: i32,
    y: i32,
};

// AutoHashMap handles hashing automatically
var point_map = std.AutoHashMap(Point, []const u8).init(allocator);
defer point_map.deinit();

try point_map.put(.{ .x = 1, .y = 2 }, "origin-ish");

Custom Hash Function for HashMap

const std = @import("std");

const MyKey = struct {
    id: u64,
    name: []const u8,
};

const MyContext = struct {
    pub fn hash(self: @This(), key: MyKey) u64 {
        _ = self;
        var h = std.hash.Wyhash.init(0);
        h.update(std.mem.asBytes(&key.id));
        h.update(key.name);
        return h.final();
    }

    pub fn eql(self: @This(), a: MyKey, b: MyKey) bool {
        _ = self;
        return a.id == b.id and std.mem.eql(u8, a.name, b.name);
    }
};

var map = std.HashMap(MyKey, u32, MyContext, 80).init(allocator);

Hash Function Details

Wyhash

Default hash for std.HashMap. Very fast with excellent distribution.

const std = @import("std");
const Wyhash = std.hash.Wyhash;

// One-shot (most efficient for single use)
const h = Wyhash.hash(seed, data);

// Streaming
var hasher = Wyhash.init(seed);
hasher.update(chunk1);
hasher.update(chunk2);
const result = hasher.final();  // idempotent, can call multiple times

XxHash Family

High-performance hash functions by Yann Collet:

const std = @import("std");

// XxHash3 - fastest for large inputs
const h1 = std.hash.XxHash3.hash(0, data);

// XxHash64 - 64-bit output
const h2 = std.hash.XxHash64.hash(0, data);

// XxHash32 - 32-bit output
const h3 = std.hash.XxHash32.hash(0, data);

// Streaming
var hasher = std.hash.XxHash64.init(0);
hasher.update(chunk);
const result = hasher.final();

FNV-1a

Simple, portable hash. Good for small data:

const std = @import("std");

// One-shot
const h32 = std.hash.Fnv1a_32.hash("data");
const h64 = std.hash.Fnv1a_64.hash("data");
const h128 = std.hash.Fnv1a_128.hash("data");

// Streaming
var hasher = std.hash.Fnv1a_64.init();
hasher.update("hello ");
hasher.update("world");
const result = hasher.final();

Murmur Hash

Well-tested, portable hash functions:

const std = @import("std");
const murmur = std.hash.murmur;

// Murmur2
const h1 = murmur.Murmur2_32.hash("data");
const h2 = murmur.Murmur2_64.hash("data");

// Murmur3
const h3 = murmur.Murmur3_32.hash("data");

// With seed
const h4 = murmur.Murmur2_32.hashWithSeed("data", 12345);
const h5 = murmur.Murmur2_64.hashWithSeed("data", 12345);

// Direct integer hashing
const h6 = murmur.Murmur2_32.hashUint32(12345);
const h7 = murmur.Murmur2_64.hashUint64(12345);

CityHash

Google's fast hash function:

const std = @import("std");
const cityhash = std.hash.cityhash;

const h32 = cityhash.CityHash32.hash("data");
const h64 = cityhash.CityHash64.hash("data");

// With seed
const h64_seeded = cityhash.CityHash64.hashWithSeed("data", 12345);

SipHash

Cryptographically strong for hash table protection:

const std = @import("std");

// Requires 128-bit key
const key: [16]u8 = .{0} ** 16;

var sip64 = std.hash.SipHash64(2, 4).init(&key);
sip64.update("data");
const h64 = sip64.finalInt();

var sip128 = std.hash.SipHash128(2, 4).init(&key);
sip128.update("data");
const h128 = sip128.finalInt();

// Default parameters (2-4 rounds)
const SipHash = std.hash.SipHash64(2, 4);

Common Patterns

Combine Multiple Values

fn combineHashes(a: u64, b: u64) u64 {
    var hasher = std.hash.Wyhash.init(0);
    hasher.update(std.mem.asBytes(&a));
    hasher.update(std.mem.asBytes(&b));
    return hasher.final();
}

// Or use autoHash for any type
fn hashPair(comptime T: type, a: T, b: T) u64 {
    var hasher = std.hash.Wyhash.init(0);
    std.hash.autoHash(&hasher, a);
    std.hash.autoHash(&hasher, b);
    return hasher.final();
}

File Checksum

fn checksumFile(io: std.Io, path: []const u8) !u32 {
    const file = try std.Io.Dir.cwd().openFile(io, path, .{});
    defer file.close(io);

    var crc: std.hash.Crc32 = .init();
    var reader_buf: [4096]u8 = undefined;
    var chunk: [4096]u8 = undefined;
    var reader = file.reader(io, &reader_buf);

    while (true) {
        const n = try reader.interface.readSliceShort(&chunk);
        if (n == 0) break;
        crc.update(chunk[0..n]);
    }

    return crc.final();
}

Bloom Filter Hash

fn bloomHashes(data: []const u8, hashes: []u64) void {
    const h1 = std.hash.Wyhash.hash(0, data);
    const h2 = std.hash.Wyhash.hash(h1, data);

    for (hashes, 0..) |*hash, i| {
        hash.* = h1 +% @as(u64, i) *% h2;
    }
}

Consistent Hashing

fn consistentHash(key: []const u8, num_buckets: u32) u32 {
    const hash = std.hash.XxHash64.hash(0, key);
    // Jump consistent hash
    var b: i64 = -1;
    var j: i64 = 0;
    var h = hash;
    while (j < num_buckets) {
        b = j;
        h = h *% 2862933555777941757 +% 1;
        j = @intFromFloat(@as(f64, @floatFromInt(b + 1)) *
            (@as(f64, 1 << 31) / @as(f64, @floatFromInt((h >> 33) + 1))));
    }
    return @intCast(b);
}

Performance Notes

  • Wyhash: Fastest general-purpose hash, excellent for hash tables
  • XxHash3: Fastest for large inputs (>256 bytes), uses SIMD when available
  • XxHash64/32: Good balance of speed and portability
  • Fnv1a: Simple, small code size, slower for large data
  • Murmur: Widely compatible, good for cross-platform consistency
  • CityHash: Fast, optimized for x86
  • Crc32: Hardware-accelerated on many platforms
  • Adler32: Fastest checksum, weaker error detection

Notes

  • All hash functions are deterministic (same input = same output)
  • Non-cryptographic hashes are NOT suitable for security (use std.crypto.hash)
  • autoHash rejects slices by default to avoid ambiguity; use autoHashStrat with explicit strategy
  • Streaming APIs (init/update/final) allow hashing data incrementally
  • final() is idempotent on most hashers (can be called multiple times)
  • For HashMap keys, implement custom hash and eql in a context struct