| /* |
| * 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 */ |