blob: 2f3f256e035332e1553c63ca88928b0303ab89e4 [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.c - Robin Hood open-addressing hashtable for AGE.
*
* See agehash.h for the public contract. This file implements the INLINE
* mode only.
*
* Internal slot layout (INLINE):
*
* bytes 0..1 uint16 probe_dist (AGEHASH_EMPTY = 0xFFFF marks empty)
* bytes 2..3 uint16 reserved (future tombstone / flag bits)
* bytes 4..7 uint32 pad (forces key to 8-byte alignment)
* bytes 8..K+7 key
* bytes K+8.. payload
*
* slot_size = MAXALIGN(8 + key_size + payload_size).
*/
#include "postgres.h"
#include "fmgr.h"
#include "utils/agehash.h"
#include "utils/builtins.h"
#include "utils/memutils.h"
/* ------------------------------------------------------------------------- */
struct AgeHashTable
{
/* Slot array: capacity * slot_size bytes, palloc'd in mcxt. */
char *slots;
uint32 capacity; /* always a power of two */
uint32 capacity_mask; /* capacity - 1 */
uint32 size; /* live entries */
uint32 max_size; /* size at which we grow */
uint32 slot_size; /* total bytes per slot */
uint32 key_size;
uint32 payload_size;
uint32 payload_offset; /* AGEHASH_SLOT_KEY_OFFSET + key_size */
AgeHashMode mode;
bool frozen;
agehash_hash_fn hash_fn;
agehash_keyeq_fn keyeq_fn;
MemoryContext mcxt;
};
/* ------------------------------------------------------------------------- */
/* Slot accessors. */
static inline char *
slot_at(AgeHashTable *t, uint32 idx)
{
return t->slots + (Size) idx * t->slot_size;
}
static inline uint16
slot_probe_dist(const char *slot)
{
uint16 d;
memcpy(&d, slot, sizeof(uint16));
return d;
}
static inline void
slot_set_probe_dist(char *slot, uint16 d)
{
memcpy(slot, &d, sizeof(uint16));
}
static inline char *
slot_key_ptr(AgeHashTable *t, char *slot)
{
(void) t;
return slot + AGEHASH_SLOT_KEY_OFFSET;
}
static inline char *
slot_payload_ptr(AgeHashTable *t, char *slot)
{
return slot + t->payload_offset;
}
/* ------------------------------------------------------------------------- */
/* Construction. */
static uint32
next_pow2(uint32 v)
{
uint32 p = 1;
while (p < v)
p <<= 1;
return p;
}
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)
{
AgeHashTable *t;
MemoryContext oldctx;
uint32 min_cap;
uint32 cap;
Assert(mcxt != NULL);
Assert(key_size > 0 && key_size <= 64);
Assert(payload_size > 0 && payload_size <= 4096);
Assert(hash_fn != NULL);
Assert(keyeq_fn != NULL);
/*
* Runtime enforcement of the on-stack carrier limits used by the inline
* Robin Hood path (carry_key[64], carry_payload[4096]). Asserts above
* give early diagnostics in debug builds; this elog covers production
* builds where Asserts compile out and the same caller would otherwise
* trigger a stack-buffer overflow during insert.
*/
if (key_size == 0 || key_size > 64)
{
ereport(ERROR,
(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
errmsg("agehash inline key size %zu out of range (must be 1..64)",
(size_t) key_size)));
}
if (payload_size == 0 || payload_size > 4096)
{
ereport(ERROR,
(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
errmsg("agehash inline payload size %zu out of range (must be 1..4096)",
(size_t) payload_size)));
}
oldctx = MemoryContextSwitchTo(mcxt);
t = palloc0(sizeof(AgeHashTable));
t->mcxt = mcxt;
t->mode = AGEHASH_INLINE;
t->frozen = false;
t->hash_fn = hash_fn;
t->keyeq_fn = keyeq_fn;
t->key_size = (uint32) key_size;
t->payload_size = (uint32) payload_size;
/*
* MAXALIGN payload_offset so that the typed payload pointer returned
* by slot_payload_ptr() is suitably aligned for any C type the caller
* might cast to. Without this, a key_size that is not a multiple of
* MAXIMUM_ALIGNOF (e.g. a 12-byte key) would leave the payload at a
* misaligned address — undefined behavior under strict alignment rules
* even though it works in practice on x86_64.
*/
t->payload_offset = MAXALIGN(AGEHASH_SLOT_KEY_OFFSET + (uint32) key_size);
t->slot_size = MAXALIGN(t->payload_offset + (uint32) payload_size);
/*
* agehash_key_from_payload() recovers the key as (payload - key_size),
* which is only valid when the payload abuts the key with no MAXALIGN
* padding between them. That holds iff key_size is a multiple of
* MAXIMUM_ALIGNOF. Enforce the invariant here so a future non-aligned
* key trips in DEBUG builds rather than silently handing the macro a
* wrong pointer.
*/
if (key_size % MAXIMUM_ALIGNOF != 0)
{
ereport(ERROR,
(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
errmsg("agehash inline key size %zu must be a multiple of %d for key recovery",
(size_t) key_size, MAXIMUM_ALIGNOF)));
}
Assert(t->payload_offset == AGEHASH_SLOT_KEY_OFFSET + (uint32) key_size);
/*
* Capacity floor of 64 keeps tiny tables out of degenerate-load territory
* and avoids a flurry of grows on the first few inserts.
*/
if (capacity_hint == 0)
min_cap = 64;
else
{
/* size capacity_hint at MAX_LOAD so we don't immediately grow */
min_cap = (uint32) ((double) capacity_hint / AGEHASH_MAX_LOAD) + 1;
if (min_cap < 64)
min_cap = 64;
}
cap = next_pow2(min_cap);
Assert((cap & (cap - 1)) == 0);
t->capacity = cap;
t->capacity_mask = cap - 1;
t->size = 0;
t->max_size = (uint32) ((double) cap * AGEHASH_MAX_LOAD);
/*
* The slot array can comfortably exceed 1 GiB on production graphs
* (the SF3 ldbc_snb edge_table is multiple GiB at the 0.85 load
* factor). Use the HUGE allocator to bypass the standard MaxAllocSize
* check.
*/
t->slots = (char *) MemoryContextAllocHuge(mcxt,
(Size) cap * t->slot_size);
/* Mark every slot empty. */
{
uint32 i;
for (i = 0; i < cap; i++)
slot_set_probe_dist(slot_at(t, i), AGEHASH_EMPTY);
}
MemoryContextSwitchTo(oldctx);
return t;
}
/* ------------------------------------------------------------------------- */
/* Insert. Robin Hood with rich-poor swap. */
static void agehash_grow(AgeHashTable *t);
static void *
agehash_insert_internal(AgeHashTable *t, const void *key, uint32 hashvalue,
bool *found)
{
uint32 i;
uint16 d;
/*
* Carrier for the entry currently being placed. Starts as the caller's
* key with a fresh, zero-filled payload; gets overwritten on each
* Robin Hood swap.
*/
char carry_key[64];
char carry_payload[4096];
void *result_payload = NULL;
bool placed_caller = false;
/*
* agehash_freeze() is documented to make subsequent inserts/grows fail
* loudly. The Assert below catches violations in debug builds; the
* runtime check guarantees the contract in production builds where
* Asserts compile out, preventing silent slot reordering that would
* invalidate any payload pointers callers consider stable post-freeze.
*/
if (t->frozen)
{
elog(ERROR, "agehash: insert into frozen table");
}
Assert(!t->frozen);
Assert(t->key_size <= sizeof(carry_key));
Assert(t->payload_size <= sizeof(carry_payload));
/* Grow before insert if at threshold. */
if (t->size >= t->max_size)
agehash_grow(t);
/* Initialize carry buffers with the caller's key and an empty payload. */
memcpy(carry_key, key, t->key_size);
memset(carry_payload, 0, t->payload_size);
i = hashvalue & t->capacity_mask;
d = 0;
for (;;)
{
char *slot = slot_at(t, i);
uint16 sd = slot_probe_dist(slot);
if (sd == AGEHASH_EMPTY)
{
/* Place the carrier here and we're done. */
slot_set_probe_dist(slot, d);
memcpy(slot_key_ptr(t, slot), carry_key, t->key_size);
memcpy(slot_payload_ptr(t, slot), carry_payload, t->payload_size);
t->size++;
if (!placed_caller)
{
/* The caller's slot landed here. */
if (found != NULL)
*found = false;
return slot_payload_ptr(t, slot);
}
/*
* The caller was placed earlier via a swap; result_payload
* already points at their final slot.
*/
Assert(result_payload != NULL);
return result_payload;
}
if (sd == d &&
!placed_caller &&
t->keyeq_fn(slot_key_ptr(t, slot), carry_key, t->key_size))
{
/*
* Existing entry with the caller's key. Note: this match check
* is only relevant before we've performed a swap; once we've
* placed the caller into a slot, the key in `carry` is some
* displaced entry that, by RH invariant on insert from a fresh
* key, cannot already exist in the table.
*/
if (found != NULL)
*found = true;
return slot_payload_ptr(t, slot);
}
if (sd < d)
{
/*
* Rich-poor swap: this slot's owner is closer to its ideal
* bucket than we are. Take its place and continue with the
* displaced entry. If we have not yet placed the caller, this
* is where they end up; remember the pointer so we can return
* it once the displaced chain finishes.
*/
char tmp_key[64];
char tmp_payload[4096];
uint16 tmp_d = sd;
memcpy(tmp_key, slot_key_ptr(t, slot), t->key_size);
memcpy(tmp_payload, slot_payload_ptr(t, slot), t->payload_size);
slot_set_probe_dist(slot, d);
memcpy(slot_key_ptr(t, slot), carry_key, t->key_size);
memcpy(slot_payload_ptr(t, slot), carry_payload, t->payload_size);
if (!placed_caller)
{
placed_caller = true;
result_payload = slot_payload_ptr(t, slot);
/* Notify caller: this insert is a fresh entry. */
if (found != NULL)
{
*found = false;
found = NULL; /* don't write again */
}
}
/* Continue with the displaced entry as the new carrier. */
memcpy(carry_key, tmp_key, t->key_size);
memcpy(carry_payload, tmp_payload, t->payload_size);
d = tmp_d;
}
i = (i + 1) & t->capacity_mask;
d++;
/*
* Probe distance overflow guard. With AGEHASH_MAX_LOAD = 0.85 and a
* non-degenerate hash function, max probe is expected to remain far
* below this limit in practice. The 0xFE00 ceiling reserves
* headroom while leaving probe_dist well clear of the AGEHASH_EMPTY
* sentinel.
*/
Assert(d < 0xFE00);
if (unlikely(d >= 0xFE00))
elog(ERROR, "agehash: probe distance overflow (likely a bad hash function)");
}
}
void *
agehash_insert(AgeHashTable *t, const void *key, bool *found)
{
uint32 h = t->hash_fn(key, t->key_size);
return agehash_insert_internal(t, key, h, found);
}
void *
agehash_insert_with_hash(AgeHashTable *t, const void *key,
uint32 hashvalue, bool *found)
{
return agehash_insert_internal(t, key, hashvalue, found);
}
/* ------------------------------------------------------------------------- */
/* Grow: double the capacity and rehash. */
static void
agehash_grow(AgeHashTable *t)
{
char *old_slots;
uint32 old_cap;
uint32 old_slot_size;
uint32 new_cap;
MemoryContext oldctx;
uint32 i;
old_slots = t->slots;
old_cap = t->capacity;
old_slot_size = t->slot_size;
if (t->frozen)
{
elog(ERROR, "agehash: grow on frozen table");
}
Assert(!t->frozen);
if (old_cap > (UINT32_MAX >> 1))
{
ereport(ERROR,
(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
errmsg("agehash capacity overflow: cannot grow beyond %u slots",
old_cap)));
}
new_cap = old_cap << 1;
Assert(new_cap > old_cap); /* overflow guard */
oldctx = MemoryContextSwitchTo(t->mcxt);
t->capacity = new_cap;
t->capacity_mask = new_cap - 1;
t->max_size = (uint32) ((double) new_cap * AGEHASH_MAX_LOAD);
/* HUGE allocator: see agehash_create_inline for the rationale. */
t->slots = (char *) MemoryContextAllocHuge(t->mcxt,
(Size) new_cap * t->slot_size);
for (i = 0; i < new_cap; i++)
slot_set_probe_dist(slot_at(t, i), AGEHASH_EMPTY);
/* Reset size; we re-insert below (which will increment it). */
t->size = 0;
for (i = 0; i < old_cap; i++)
{
char *src = old_slots + (Size) i * old_slot_size;
if (slot_probe_dist(src) != AGEHASH_EMPTY)
{
void *src_key = src + AGEHASH_SLOT_KEY_OFFSET;
void *src_payload = src + t->payload_offset;
uint32 h = t->hash_fn(src_key, t->key_size);
void *dst_payload = agehash_insert_internal(t, src_key, h, NULL);
memcpy(dst_payload, src_payload, t->payload_size);
}
}
pfree(old_slots);
MemoryContextSwitchTo(oldctx);
}
/* ------------------------------------------------------------------------- */
/* Lookup. */
void *
agehash_lookup_with_hash(AgeHashTable *t, const void *key, uint32 hashvalue)
{
uint32 i = hashvalue & t->capacity_mask;
uint16 d = 0;
for (;;)
{
char *slot = slot_at(t, i);
uint16 sd = slot_probe_dist(slot);
if (sd == AGEHASH_EMPTY)
return NULL;
/*
* Robin Hood invariant: probe_dist values along a probe sequence
* are non-increasing as we move from an entry's home slot. If the
* slot we land on has a smaller probe_dist than ours, the key
* we're looking for can't be anywhere later in the sequence.
*/
if (sd < d)
return NULL;
if (t->keyeq_fn(slot_key_ptr(t, slot), key, t->key_size))
return slot_payload_ptr(t, slot);
i = (i + 1) & t->capacity_mask;
d++;
Assert(d < 0xFE00);
}
}
void *
agehash_lookup(AgeHashTable *t, const void *key)
{
uint32 h = t->hash_fn(key, t->key_size);
return agehash_lookup_with_hash(t, key, h);
}
/* ------------------------------------------------------------------------- */
/* Misc accessors. */
void
agehash_freeze(AgeHashTable *t)
{
t->frozen = true;
}
bool
agehash_is_frozen(const AgeHashTable *t)
{
return t->frozen;
}
uint32
agehash_size(const AgeHashTable *t)
{
return t->size;
}
uint32
agehash_capacity(const AgeHashTable *t)
{
return t->capacity;
}
void
agehash_iter_init(AgeHashTable *t, AgeHashIter *it)
{
it->t = t;
it->idx = 0;
it->key = NULL;
it->payload = NULL;
}
bool
agehash_iter_next(AgeHashIter *it)
{
AgeHashTable *t = it->t;
while (it->idx < t->capacity)
{
char *slot = slot_at(t, it->idx);
uint32 idx = it->idx++;
(void) idx;
if (slot_probe_dist(slot) != AGEHASH_EMPTY)
{
it->key = slot_key_ptr(t, slot);
it->payload = slot_payload_ptr(t, slot);
return true;
}
}
it->key = NULL;
it->payload = NULL;
return false;
}
/* ------------------------------------------------------------------------- */
/* Self-test. Exercises insert / lookup / grow / iterate at small + medium
* sizes and verifies invariants. Returns a string in CurrentMemoryContext. */
/* MurmurHash3 fmix64, identical to graphid_hash. */
static uint32
selftest_hash(const void *key, Size keysize)
{
uint64 k;
Assert(keysize == sizeof(uint64));
memcpy(&k, key, sizeof(uint64));
k ^= k >> 33;
k *= UINT64CONST(0xff51afd7ed558ccd);
k ^= k >> 33;
k *= UINT64CONST(0xc4ceb9fe1a85ec53);
k ^= k >> 33;
return (uint32) k;
}
static bool
selftest_keyeq(const void *a, const void *b, Size keysize)
{
return memcmp(a, b, keysize) == 0;
}
typedef struct selftest_payload
{
uint64 mirror_key;
uint64 marker;
} selftest_payload;
static const char *
selftest_run_one(MemoryContext parent, uint32 n, uint32 hint)
{
MemoryContext mcxt;
AgeHashTable *t;
selftest_payload *p;
bool found;
uint32 i;
uint32 seen;
AgeHashIter it;
mcxt = AllocSetContextCreate(parent, "agehash selftest", ALLOCSET_DEFAULT_SIZES);
t = agehash_create_inline(mcxt, sizeof(uint64), sizeof(selftest_payload),
hint, selftest_hash, selftest_keyeq);
/* Insert n keys. */
for (i = 0; i < n; i++)
{
uint64 k = ((uint64) 0xa5a5 << 48) | (i + 1);
p = (selftest_payload *) agehash_insert(t, &k, &found);
if (found)
{
MemoryContextDelete(mcxt);
return psprintf("FAIL: duplicate insert at i=%u", i);
}
p->mirror_key = k;
p->marker = (uint64) 0xdeadbeef00000000ULL | i;
}
if (agehash_size(t) != n)
{
MemoryContextDelete(mcxt);
return psprintf("FAIL: size %u != %u after inserts",
agehash_size(t), n);
}
/* Lookup all n keys. */
for (i = 0; i < n; i++)
{
uint64 k = ((uint64) 0xa5a5 << 48) | (i + 1);
p = (selftest_payload *) agehash_lookup(t, &k);
if (p == NULL)
{
MemoryContextDelete(mcxt);
return psprintf("FAIL: lookup miss at i=%u", i);
}
if (p->mirror_key != k ||
p->marker != ((uint64) 0xdeadbeef00000000ULL | i))
{
MemoryContextDelete(mcxt);
return psprintf("FAIL: payload corruption at i=%u", i);
}
}
/* Lookup n keys that should not exist. */
for (i = 0; i < n; i++)
{
uint64 k = ((uint64) 0xb6b6 << 48) | (i + 1);
p = (selftest_payload *) agehash_lookup(t, &k);
if (p != NULL)
{
MemoryContextDelete(mcxt);
return psprintf("FAIL: phantom lookup hit at i=%u", i);
}
}
/* Re-insert (HASH_ENTER semantics) — should report found = true. */
for (i = 0; i < n; i++)
{
uint64 k = ((uint64) 0xa5a5 << 48) | (i + 1);
p = (selftest_payload *) agehash_insert(t, &k, &found);
if (!found)
{
MemoryContextDelete(mcxt);
return psprintf("FAIL: re-insert reported !found at i=%u", i);
}
if (p->mirror_key != k)
{
MemoryContextDelete(mcxt);
return psprintf("FAIL: re-insert payload mismatch at i=%u", i);
}
}
if (agehash_size(t) != n)
{
MemoryContextDelete(mcxt);
return psprintf("FAIL: size %u != %u after re-inserts",
agehash_size(t), n);
}
/* Iterate and count. */
seen = 0;
agehash_iter_init(t, &it);
while (agehash_iter_next(&it))
{
selftest_payload *pp = it.payload;
uint64 k;
memcpy(&k, it.key, sizeof(uint64));
if (pp->mirror_key != k)
{
MemoryContextDelete(mcxt);
return psprintf("FAIL: iter payload mismatch at seen=%u", seen);
}
seen++;
}
if (seen != n)
{
MemoryContextDelete(mcxt);
return psprintf("FAIL: iter saw %u of %u", seen, n);
}
/* Freeze and confirm lookups still work. */
agehash_freeze(t);
if (!agehash_is_frozen(t))
{
MemoryContextDelete(mcxt);
return "FAIL: agehash_is_frozen returned false after freeze";
}
{
uint64 k = ((uint64) 0xa5a5 << 48) | 1;
p = (selftest_payload *) agehash_lookup(t, &k);
if (p == NULL)
{
MemoryContextDelete(mcxt);
return "FAIL: lookup failed after freeze";
}
}
MemoryContextDelete(mcxt);
return NULL; /* OK */
}
const char *
agehash_self_test(void)
{
static const struct { uint32 n; uint32 hint; } cases[] = {
{ 1, 0 },
{ 7, 0 },
{ 8, 0 },
{ 9, 0 },
{ 63, 0 },
{ 64, 0 },
{ 65, 0 },
{ 1023, 0 }, /* forces grow from 64 floor */
{ 1024, 0 },
{ 1025, 0 },
{ 10000, 0 }, /* forces multiple grows */
{ 10000, 8192 }, /* with capacity hint, no grow expected */
{ 50000, 0 }, /* larger; multiple grows */
{ 1000000, 0 }, /* exercises grow at multi-MB allocations */
/*
* NOTE: this set is bounded so 'make installcheck' completes
* quickly. The library has been manually verified up to 256M
* entries (multi-GiB slot arrays via MemoryContextAllocHuge).
*/
};
const size_t ncases = sizeof(cases) / sizeof(cases[0]);
size_t i;
for (i = 0; i < ncases; i++)
{
const char *r = selftest_run_one(CurrentMemoryContext,
cases[i].n, cases[i].hint);
if (r != NULL)
return psprintf("%s [n=%u hint=%u]", r, cases[i].n, cases[i].hint);
}
return "OK";
}
/* ------------------------------------------------------------------------- */
/* SQL-callable wrapper: SELECT ag_catalog._agehash_self_test(); */
PG_FUNCTION_INFO_V1(_agehash_self_test);
Datum
_agehash_self_test(PG_FUNCTION_ARGS)
{
const char *r = agehash_self_test();
PG_RETURN_TEXT_P(cstring_to_text(r));
}