blob: 9f0ba2793f52c1ec2c0086b098f813590c6a482a [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/*
* agehash.h - Robin Hood open-addressing hashtable for AGE's hot-path caches.
*
* This is an internal utility used by the global graph cache (vertex and edge
* tables) to replace dynahash on the lookup-dominated read-only-after-build
* path. Each lookup traverses a single contiguous slot array with no chain
* pointer-chasing, which on AGE workloads roughly halves lookup latency
* relative to dynahash (see Stage 5 microbench).
*
* Mode: only AGEHASH_INLINE is supported by this initial revision. INLINE
* stores the payload directly in the slot, suitable for tables that are
* never mutated after the build phase. A future revision will add an
* AGEHASH_INDIRECT mode for tables that support insert and pointer-stable
* payloads during queries.
*
* Memory: every allocation lives in a caller-supplied MemoryContext. Free is
* a single MemoryContextDelete by the caller; agehash itself never frees
* piecewise. This makes leak-on-elog impossible: PG unwinds the context.
*
* Capacity: always a power of two; grown by doubling when size exceeds
* AGEHASH_MAX_LOAD * capacity. After agehash_freeze() is called, all
* insert paths are forbidden (asserted in DEBUG builds), which guarantees
* that lookups can never observe a partially-rehashed structure.
*/
#ifndef AG_AGEHASH_H
#define AG_AGEHASH_H
#include "postgres.h"
#include "utils/memutils.h"
#include "utils/palloc.h"
/* Sentinel probe distance marking an empty slot. */
#define AGEHASH_EMPTY 0xFFFFu
/*
* Load factor above which we grow.
*
* 0.85 balances three goals on AGE's hot tables:
* - Memory: the edge_table on SF3 is ~52M entries; at 0.85 the slot array
* is ~67M slots which is roughly the same total bytes as the dynahash
* bucket array + per-entry HASHELEMENT chain headers it replaces.
* - Probe distance: Robin Hood at 0.85 still keeps average probes near 1
* and max probes well below the 0xFE00 overflow guard.
* - Grow cadence: a higher threshold means fewer doublings during the
* edge cache build (each doubling rehashes the entire table).
*
* If you change this, re-run the rh_microbench harness on the VM and the
* SF3 paired benchmark; both are sensitive to the load factor.
*/
#define AGEHASH_MAX_LOAD 0.85
/*
* Caller-supplied hash callback. keysize is constant for a given table; we
* still pass it so callers can reuse one function across multiple tables
* with different key types if desired.
*/
typedef uint32 (*agehash_hash_fn)(const void *key, Size keysize);
/* Caller-supplied key-equality callback. Returns true iff a == b. */
typedef bool (*agehash_keyeq_fn)(const void *a, const void *b, Size keysize);
/*
* Layout mode. Only INLINE is implemented in this revision; INDIRECT is
* declared so the public enum values stay stable when it lands.
*/
typedef enum AgeHashMode
{
AGEHASH_INLINE = 0,
AGEHASH_INDIRECT = 1
} AgeHashMode;
/* Opaque table handle. */
typedef struct AgeHashTable AgeHashTable;
/*
* Slot layout (INLINE mode), packed:
*
* offset 0 : uint16 probe_dist (AGEHASH_EMPTY == empty)
* offset 2 : uint16 _reserved (future flags / tombstones)
* offset 4 : uint32 _pad (force key to 8-byte alignment)
* offset 8 : key (key_size bytes)
* offset 8+key_size : payload (payload_size bytes)
*
* The header is 8 bytes; total slot bytes = 8 + key_size + payload_size,
* rounded up to MAXIMUM_ALIGNOF.
*/
#define AGEHASH_SLOT_HDR_BYTES 8
#define AGEHASH_SLOT_KEY_OFFSET AGEHASH_SLOT_HDR_BYTES
/*
* Recover a key pointer from a payload pointer. INLINE-mode tables store
* the key immediately before the payload, so this is pure pointer
* arithmetic and does not need the table handle. The caller must know the
* key size at this site; this is the case for every AGE caller (each table
* has a single fixed key type).
*
* Invariant: this is correct only when the payload directly abuts the key,
* i.e. when payload_offset == AGEHASH_SLOT_KEY_OFFSET + key_size with no
* MAXALIGN padding. That holds iff key_size is a multiple of
* MAXIMUM_ALIGNOF (all current AGE callers use an 8-byte graphid key).
* agehash_create_inline() asserts this invariant in DEBUG builds.
*/
#define agehash_key_from_payload(payload, key_size) \
((const void *) ((const char *) (payload) - (Size) (key_size)))
/*
* Construction. capacity_hint is a number of entries; the actual capacity
* will be the next power of two >= capacity_hint / AGEHASH_MAX_LOAD, with a
* floor of 64 slots. Pass 0 to let the table start at the floor.
*/
extern AgeHashTable *agehash_create_inline(MemoryContext mcxt,
Size key_size,
Size payload_size,
uint32 capacity_hint,
agehash_hash_fn hash_fn,
agehash_keyeq_fn keyeq_fn);
/*
* Reserve / find. If the key is not present, allocates a fresh slot
* (rebalancing via Robin Hood swaps), zero-fills the payload, sets
* *found = false, and returns a pointer to the payload region. The caller
* fills it in. If the key is present, sets *found = true and returns the
* existing payload pointer.
*
* The returned payload pointer is *not* stable across subsequent
* agehash_insert calls in INLINE mode (a later insert may swap this slot).
* Callers requiring stable pointers must use INDIRECT mode (future).
*
* Asserts that the table has not been frozen (DEBUG builds).
*/
extern void *agehash_insert(AgeHashTable *t, const void *key, bool *found);
/* Variant that accepts a precomputed hash value, skipping the hash callback. */
extern void *agehash_insert_with_hash(AgeHashTable *t, const void *key,
uint32 hashvalue, bool *found);
/*
* Lookup. Returns a pointer to the payload region, or NULL if absent.
* The pointer is stable as long as no further insert touches the table.
*/
extern void *agehash_lookup(AgeHashTable *t, const void *key);
/* Variant accepting a precomputed hash value. */
extern void *agehash_lookup_with_hash(AgeHashTable *t, const void *key,
uint32 hashvalue);
/*
* Freeze the table: subsequent insert/grow attempts are an Assert failure
* in DEBUG and an elog(ERROR) in production. This is the contract that
* lets read-only-after-build callers hand out long-lived payload pointers.
*/
extern void agehash_freeze(AgeHashTable *t);
/* True after agehash_freeze(); useful for caller-side asserts. */
extern bool agehash_is_frozen(const AgeHashTable *t);
/* Live entry count. */
extern uint32 agehash_size(const AgeHashTable *t);
/* Allocated slot count (capacity). */
extern uint32 agehash_capacity(const AgeHashTable *t);
/*
* Iteration. Usage:
*
* AgeHashIter it;
* for (agehash_iter_init(t, &it); agehash_iter_next(&it); )
* {
* graphid k = *(graphid *) it.key;
* my_payload *p = it.payload;
* ...
* }
*
* Iteration order is unspecified. Modifying the table during iteration is
* undefined behaviour.
*/
typedef struct AgeHashIter
{
AgeHashTable *t;
uint32 idx;
void *key;
void *payload;
} AgeHashIter;
extern void agehash_iter_init(AgeHashTable *t, AgeHashIter *it);
extern bool agehash_iter_next(AgeHashIter *it);
/*
* Internal self-test. Returns a NUL-terminated diagnostic string allocated
* in CurrentMemoryContext: "OK" on success, "FAIL: <reason>" on failure.
* Used by the agehash regression test.
*/
extern const char *agehash_self_test(void);
#endif /* AG_AGEHASH_H */