Compare commits
6
Commits
8fa1b39d12
...
3ecc491022
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ecc491022 | ||
|
|
3efd0bf037 | ||
|
|
69151d54dd | ||
|
|
da43582394 | ||
|
|
e8d99cf676 | ||
|
|
97353ebddc |
@@ -1,7 +1,31 @@
|
||||
В этой реалзации попытка хранить хэш функции напрямую, без кодирования
|
||||
В этой реализации хэши хранятся напрямую, без Golomb-Rice-кодирования.
|
||||
|
||||
В теории это должно увеличить размер set-строки ~x2, но
|
||||
Set-строка целиком декодируется и проверяется при первом обращении. Как и в
|
||||
`set9.c`, полностью декодированные массивы хэшей сохраняются в двух LRU-кэшах
|
||||
по 512 записей — отдельно для первого и второго операнда.
|
||||
|
||||
- позволить индексироваться по элементам хэша
|
||||
- кратно сократить время на дешифровку
|
||||
- кэшировать можно соответствия индекс - значение, чтобы не работать с битами (может и не стоит того)
|
||||
Для 32-битного формата Base64
|
||||
декодируется сразу в три `unsigned` за блок с одновременной проверкой порядка.
|
||||
Большое понижение BPP выполняется radix-сортировкой, а не отдельным проходом на
|
||||
каждый бит.
|
||||
|
||||
Последний запуск `taskset -c 2 python3 benchmark.py`:
|
||||
|
||||
```text
|
||||
symbols=1000 required=500 bpp=32
|
||||
implementation set_chars format
|
||||
set9 3994 golomb/base62
|
||||
direct 5340 D1/base64
|
||||
|
||||
operation set9 direct direct/set9
|
||||
set_fini only 147.45 us 105.09 us 0.71x
|
||||
new+add (ctypes) 611.28 us 608.24 us 1.00x
|
||||
new+add+fini (ctypes) 769.66 us 693.60 us 0.90x
|
||||
rpmsetcmp cold 129.67 us 113.04 us 0.87x
|
||||
rpmsetcmp warm 2.99 us 1.04 us 0.35x
|
||||
```
|
||||
|
||||
Для разреженного сравнения
|
||||
(`taskset -c 2 python3 benchmark.py --required 1`) получено
|
||||
`83.22 us` на холодном кэше и `0.86 us` на прогретом: соответственно `0.88x`
|
||||
и `1.02x` от времени `set9`.
|
||||
|
||||
Executable
+271
@@ -0,0 +1,271 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import ctypes
|
||||
import gc
|
||||
import os
|
||||
import statistics
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
BUILD = HERE / "build"
|
||||
LIBC = ctypes.CDLL(None)
|
||||
LIBC.free.argtypes = [ctypes.c_void_p]
|
||||
|
||||
|
||||
class SetAPI:
|
||||
def __init__(self, path: Path):
|
||||
self.lib = ctypes.CDLL(str(path))
|
||||
self.lib.set_new.restype = ctypes.c_void_p
|
||||
self.lib.set_add.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
|
||||
self.lib.set_fini.argtypes = [ctypes.c_void_p, ctypes.c_int]
|
||||
self.lib.set_fini.restype = ctypes.c_void_p
|
||||
self.lib.set_free.argtypes = [ctypes.c_void_p]
|
||||
self.lib.set_free.restype = ctypes.c_void_p
|
||||
self.lib.rpmsetcmp.argtypes = [ctypes.c_char_p, ctypes.c_char_p]
|
||||
self.lib.rpmsetcmp.restype = ctypes.c_int
|
||||
|
||||
def new_with_symbols(self, symbols):
|
||||
value = self.lib.set_new()
|
||||
if not value:
|
||||
raise RuntimeError("set_new returned NULL")
|
||||
for symbol in symbols:
|
||||
self.lib.set_add(value, symbol)
|
||||
return value
|
||||
|
||||
def release(self, value, result):
|
||||
if result:
|
||||
LIBC.free(result)
|
||||
self.lib.set_free(value)
|
||||
|
||||
def encode(self, symbols, bpp):
|
||||
value = self.new_with_symbols(symbols)
|
||||
result = self.lib.set_fini(value, bpp)
|
||||
if not result:
|
||||
raise RuntimeError("set_fini returned NULL")
|
||||
encoded = ctypes.string_at(result)
|
||||
self.release(value, result)
|
||||
return encoded
|
||||
|
||||
|
||||
def median_fini(api, symbols, bpp, calls, rounds):
|
||||
samples = []
|
||||
for _ in range(rounds):
|
||||
sets = [api.new_with_symbols(symbols) for _ in range(calls)]
|
||||
results = []
|
||||
start = time.perf_counter_ns()
|
||||
for value in sets:
|
||||
results.append(api.lib.set_fini(value, bpp))
|
||||
samples.append((time.perf_counter_ns() - start) / calls)
|
||||
if not all(results):
|
||||
raise RuntimeError("set_fini returned NULL")
|
||||
encoded = [ctypes.string_at(result) for result in results]
|
||||
if len(set(encoded)) != 1:
|
||||
raise RuntimeError("set_fini is not deterministic")
|
||||
for value, result in zip(sets, results):
|
||||
api.release(value, result)
|
||||
return statistics.median(samples)
|
||||
|
||||
|
||||
def median_build(api, symbols, bpp, calls, rounds):
|
||||
samples = []
|
||||
for _ in range(rounds):
|
||||
sets = []
|
||||
results = []
|
||||
start = time.perf_counter_ns()
|
||||
for _ in range(calls):
|
||||
value = api.new_with_symbols(symbols)
|
||||
result = api.lib.set_fini(value, bpp)
|
||||
sets.append(value)
|
||||
results.append(result)
|
||||
samples.append((time.perf_counter_ns() - start) / calls)
|
||||
if not all(results):
|
||||
raise RuntimeError("set_fini returned NULL")
|
||||
for value, result in zip(sets, results):
|
||||
api.release(value, result)
|
||||
return statistics.median(samples)
|
||||
|
||||
|
||||
def median_add(api, symbols, calls, rounds):
|
||||
samples = []
|
||||
for _ in range(rounds):
|
||||
values = []
|
||||
start = time.perf_counter_ns()
|
||||
for _ in range(calls):
|
||||
values.append(api.new_with_symbols(symbols))
|
||||
samples.append((time.perf_counter_ns() - start) / calls)
|
||||
for value in values:
|
||||
api.lib.set_free(value)
|
||||
return statistics.median(samples)
|
||||
|
||||
|
||||
def median_cmp(api, provider, requirement, calls, rounds):
|
||||
expected = api.lib.rpmsetcmp(provider, requirement)
|
||||
if expected != 1 or api.lib.rpmsetcmp(provider, provider) != 0:
|
||||
raise RuntimeError(f"unexpected rpmsetcmp result: {expected}")
|
||||
for _ in range(100):
|
||||
api.lib.rpmsetcmp(provider, requirement)
|
||||
|
||||
samples = []
|
||||
for _ in range(rounds):
|
||||
checksum = 0
|
||||
start = time.perf_counter_ns()
|
||||
for _ in range(calls):
|
||||
checksum += api.lib.rpmsetcmp(provider, requirement)
|
||||
samples.append((time.perf_counter_ns() - start) / calls)
|
||||
if checksum != calls:
|
||||
raise RuntimeError("rpmsetcmp result changed during benchmark")
|
||||
return statistics.median(samples)
|
||||
|
||||
|
||||
def cold_cmp_once(api, provider, requirement):
|
||||
read_fd, write_fd = os.pipe()
|
||||
pid = os.fork()
|
||||
if pid == 0:
|
||||
os.close(read_fd)
|
||||
start = time.perf_counter_ns()
|
||||
result = api.lib.rpmsetcmp(provider, requirement)
|
||||
elapsed = time.perf_counter_ns() - start
|
||||
os.write(write_fd, f"{elapsed} {result}".encode())
|
||||
os.close(write_fd)
|
||||
os._exit(0)
|
||||
|
||||
os.close(write_fd)
|
||||
payload = b""
|
||||
while chunk := os.read(read_fd, 128):
|
||||
payload += chunk
|
||||
os.close(read_fd)
|
||||
_, status = os.waitpid(pid, 0)
|
||||
if status != 0:
|
||||
raise RuntimeError(f"cold rpmsetcmp child failed: status={status}")
|
||||
elapsed, result = map(int, payload.split())
|
||||
if result != 1:
|
||||
raise RuntimeError(f"unexpected cold rpmsetcmp result: {result}")
|
||||
return elapsed
|
||||
|
||||
|
||||
def median_cmp_cold(api, provider, requirement, calls, rounds):
|
||||
samples = []
|
||||
for _ in range(rounds):
|
||||
total = sum(cold_cmp_once(api, provider, requirement) for _ in range(calls))
|
||||
samples.append(total / calls)
|
||||
return statistics.median(samples)
|
||||
|
||||
|
||||
def verify_complete_decoding(api, provider, requirement):
|
||||
if api.lib.rpmsetcmp(provider, requirement) != 1:
|
||||
raise RuntimeError("provider must contain requirement")
|
||||
if api.lib.rpmsetcmp(requirement, provider) != -1:
|
||||
raise RuntimeError("requirement must be contained by provider")
|
||||
|
||||
payload_position = 4 + (len(provider) - 5) * 3 // 4
|
||||
corrupted = provider[:payload_position] + b"!" + provider[payload_position + 1 :]
|
||||
if api.lib.rpmsetcmp(corrupted, requirement) != -3:
|
||||
raise RuntimeError("first operand was not decoded and validated completely")
|
||||
if api.lib.rpmsetcmp(requirement, corrupted) != -4:
|
||||
raise RuntimeError("second operand was not decoded and validated completely")
|
||||
|
||||
|
||||
def format_time(ns):
|
||||
return f"{ns / 1000:.2f} us"
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Compare set9 and direct-hash set APIs")
|
||||
parser.add_argument("--symbols", type=int, default=1000)
|
||||
parser.add_argument(
|
||||
"--required",
|
||||
type=int,
|
||||
help="number of evenly distributed required symbols (default: every second symbol)",
|
||||
)
|
||||
parser.add_argument("--bpp", type=int, default=32)
|
||||
parser.add_argument("--rounds", type=int, default=7)
|
||||
parser.add_argument("--fini-calls", type=int, default=5)
|
||||
parser.add_argument("--cmp-calls", type=int, default=2000)
|
||||
parser.add_argument("--cold-calls", type=int, default=20)
|
||||
parser.add_argument("--skip-build", action="store_true")
|
||||
args = parser.parse_args()
|
||||
if args.symbols < 2 or not 10 <= args.bpp <= 32:
|
||||
parser.error("symbols must be >= 2 and bpp must be in 10..32")
|
||||
if args.required is not None and not 1 <= args.required < args.symbols:
|
||||
parser.error("required must be in 1..symbols-1")
|
||||
if min(args.rounds, args.fini_calls, args.cmp_calls, args.cold_calls) < 1:
|
||||
parser.error("rounds and call counts must be positive")
|
||||
|
||||
if not args.skip_build:
|
||||
subprocess.run([str(HERE / "build.sh")], check=True)
|
||||
|
||||
symbols = tuple(
|
||||
f"symbol_{i:08d}_version_ALT_{i % 97}".encode() for i in range(args.symbols)
|
||||
)
|
||||
required = (
|
||||
symbols[::2]
|
||||
if args.required is None
|
||||
else tuple(symbols[i * args.symbols // args.required] for i in range(args.required))
|
||||
)
|
||||
apis = {
|
||||
"set9": SetAPI(BUILD / "libset9.so"),
|
||||
"direct": SetAPI(BUILD / "libdirect-hash.so"),
|
||||
}
|
||||
|
||||
gc.disable()
|
||||
try:
|
||||
timings = {name: [[] for _ in range(5)] for name in apis}
|
||||
lengths = {}
|
||||
encoded = {}
|
||||
for name, api in apis.items():
|
||||
provider = api.encode(symbols, args.bpp)
|
||||
requirement = api.encode(required, args.bpp)
|
||||
encoded[name] = (provider, requirement)
|
||||
wire_format = "D1/base64" if provider.startswith(b"D1") else "golomb/base62"
|
||||
lengths[name] = (len(provider), wire_format)
|
||||
|
||||
operations = (
|
||||
lambda name, api: median_fini(api, symbols, args.bpp, args.fini_calls, 1),
|
||||
lambda name, api: median_add(api, symbols, args.fini_calls, 1),
|
||||
lambda name, api: median_build(api, symbols, args.bpp, args.fini_calls, 1),
|
||||
lambda name, api: median_cmp_cold(
|
||||
api, encoded[name][0], encoded[name][1], args.cold_calls, 1
|
||||
),
|
||||
lambda name, api: median_cmp(
|
||||
api, encoded[name][0], encoded[name][1], args.cmp_calls, 1
|
||||
),
|
||||
)
|
||||
names = tuple(apis)
|
||||
for operation_index, operation in enumerate(operations):
|
||||
for round_index in range(args.rounds):
|
||||
order = names if round_index % 2 == 0 else tuple(reversed(names))
|
||||
for name in order:
|
||||
timings[name][operation_index].append(operation(name, apis[name]))
|
||||
timings = {
|
||||
name: tuple(statistics.median(samples) for samples in operation_samples)
|
||||
for name, operation_samples in timings.items()
|
||||
}
|
||||
# Run validation after timing: forked cold samples must inherit an empty
|
||||
# decoded-set cache from the parent process.
|
||||
for name, api in apis.items():
|
||||
verify_complete_decoding(api, *encoded[name])
|
||||
finally:
|
||||
gc.enable()
|
||||
|
||||
print(f"symbols={args.symbols} required={len(required)} bpp={args.bpp}")
|
||||
print("implementation set_chars format")
|
||||
for name in apis:
|
||||
print(f"{name:<14} {lengths[name][0]:>9} {lengths[name][1]}")
|
||||
print("\noperation set9 direct direct/set9")
|
||||
labels = (
|
||||
"set_fini only",
|
||||
"new+add (ctypes)",
|
||||
"new+add+fini (ctypes)",
|
||||
"rpmsetcmp cold",
|
||||
"rpmsetcmp warm",
|
||||
)
|
||||
for index, label in enumerate(labels):
|
||||
old = timings["set9"][index]
|
||||
new = timings["direct"][index]
|
||||
print(f"{label:<22} {format_time(old):>10} {format_time(new):>10} {new / old:>12.2f}x")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+28
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||
ROOT=$(cd "$HERE/../.." && pwd)
|
||||
BUILD="$HERE/build"
|
||||
mkdir -p "$BUILD"
|
||||
touch "$BUILD/rpmlib.h" "$BUILD/system.h"
|
||||
cp "$HERE/../roaring_bitmap/set.h" "$BUILD/set.h"
|
||||
|
||||
CFLAGS=(-O2 -std=gnu11 -D_GNU_SOURCE -Wall -Wextra -Werror -I"$BUILD")
|
||||
COMPAT=(-include "$ROOT/scripts/rpmsetcmp/newset_compat.h")
|
||||
|
||||
for tool in mkset setcmp; do
|
||||
cc "${CFLAGS[@]}" "${COMPAT[@]}" \
|
||||
"$ROOT/reimplement/set9.c" "$ROOT/scripts/rpmsetcmp/$tool.c" \
|
||||
-o "$BUILD/$tool-set9"
|
||||
cc "${CFLAGS[@]}" "${COMPAT[@]}" \
|
||||
"$HERE/hash_set.c" "$ROOT/scripts/rpmsetcmp/$tool.c" \
|
||||
-o "$BUILD/$tool-direct"
|
||||
done
|
||||
|
||||
cc "${CFLAGS[@]}" -fPIC -shared "${COMPAT[@]}" \
|
||||
"$ROOT/reimplement/set9.c" -o "$BUILD/libset9.so"
|
||||
cc "${CFLAGS[@]}" -fPIC -shared "${COMPAT[@]}" \
|
||||
"$HERE/hash_set.c" -o "$BUILD/libdirect-hash.so"
|
||||
|
||||
printf 'Built tools and benchmark libraries in %s\n' "$BUILD"
|
||||
@@ -0,0 +1,949 @@
|
||||
#include <assert.h>
|
||||
#include <limits.h>
|
||||
#include <stdatomic.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include "rpmlib.h"
|
||||
#include "set.h"
|
||||
#include "system.h"
|
||||
|
||||
/*
|
||||
* This is intentionally a new set-string format. It is not compatible with
|
||||
* the Golomb-Rice/base62 strings produced by the original lib/set.c.
|
||||
*
|
||||
* D1<two decimal bpp digits><RFC 4648 base64 of packed hashes>
|
||||
*
|
||||
* Sorted unique hashes are packed least-significant bit first, using exactly
|
||||
* bpp bits per hash. Base64 is only a textual representation of those bytes;
|
||||
* there is no delta or Golomb-Rice coding.
|
||||
*/
|
||||
#define FORMAT_PREFIX "D1"
|
||||
#define FORMAT_HEADER_LEN 4
|
||||
|
||||
_Static_assert(CHAR_BIT == 8, "direct-hash format requires 8-bit bytes");
|
||||
|
||||
struct set {
|
||||
size_t cnt;
|
||||
size_t symbols_cap;
|
||||
size_t strings_len;
|
||||
size_t strings_cap;
|
||||
char* strings;
|
||||
struct symbols {
|
||||
size_t offset;
|
||||
unsigned hash;
|
||||
}* symbols_v;
|
||||
};
|
||||
|
||||
struct decoded_set {
|
||||
unsigned* hashes;
|
||||
size_t count;
|
||||
unsigned bpp;
|
||||
};
|
||||
|
||||
enum {
|
||||
DECODED_CACHE_SIZE = 512,
|
||||
DECODED_CACHE_BUCKETS = 1024,
|
||||
PAIR_CACHE_SIZE = 4,
|
||||
};
|
||||
|
||||
struct decoded_cache_entry;
|
||||
|
||||
struct pair_cache_entry {
|
||||
uint64_t other_identity;
|
||||
int result;
|
||||
};
|
||||
|
||||
struct decoded_cache_entry {
|
||||
struct decoded_cache_entry* bucket_next;
|
||||
struct decoded_cache_entry* newer;
|
||||
struct decoded_cache_entry* older;
|
||||
char* str;
|
||||
unsigned* hashes;
|
||||
size_t len;
|
||||
size_t count;
|
||||
uint32_t fingerprint;
|
||||
uint64_t identity;
|
||||
unsigned bucket;
|
||||
unsigned target_bpp;
|
||||
unsigned pair_next;
|
||||
struct pair_cache_entry pairs[PAIR_CACHE_SIZE];
|
||||
};
|
||||
|
||||
struct set_meta {
|
||||
const char* str;
|
||||
size_t len;
|
||||
unsigned bpp;
|
||||
};
|
||||
|
||||
static unsigned decoded_cache_count[2];
|
||||
static struct decoded_cache_entry* decoded_cache_buckets[2][DECODED_CACHE_BUCKETS];
|
||||
static struct decoded_cache_entry* decoded_cache_newest[2];
|
||||
static struct decoded_cache_entry* decoded_cache_oldest[2];
|
||||
static uint64_t decoded_cache_next_identity = 1;
|
||||
/* Cached arrays remain in use until comparison completes, so lookup, eviction,
|
||||
* and comparison share one lock. */
|
||||
static atomic_flag decoded_cache_lock = ATOMIC_FLAG_INIT;
|
||||
|
||||
struct set* set_new(void) {
|
||||
struct set* set = xmalloc(sizeof(*set));
|
||||
set->cnt = 0;
|
||||
set->symbols_cap = 0;
|
||||
set->strings_len = 0;
|
||||
set->strings_cap = 0;
|
||||
set->strings = NULL;
|
||||
set->symbols_v = NULL;
|
||||
|
||||
return set;
|
||||
}
|
||||
|
||||
void set_add(struct set* set, const char* sym) {
|
||||
if (set->cnt == set->symbols_cap) {
|
||||
set->symbols_cap += 1024;
|
||||
set->symbols_v = xrealloc(set->symbols_v, sizeof(*set->symbols_v) * set->symbols_cap);
|
||||
}
|
||||
|
||||
size_t length = strlen(sym) + 1;
|
||||
size_t required = set->strings_len + length;
|
||||
if (required > set->strings_cap) {
|
||||
size_t capacity = set->strings_cap ? set->strings_cap : 4096;
|
||||
while (capacity < required) capacity *= 2;
|
||||
set->strings = xrealloc(set->strings, capacity);
|
||||
set->strings_cap = capacity;
|
||||
}
|
||||
|
||||
set->symbols_v[set->cnt].offset = set->strings_len;
|
||||
set->symbols_v[set->cnt].hash = 0;
|
||||
memcpy(set->strings + set->strings_len, sym, length);
|
||||
set->strings_len = required;
|
||||
++set->cnt;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
struct set* set_free(struct set* set) {
|
||||
if (set) {
|
||||
_free(set->strings);
|
||||
_free(set->symbols_v);
|
||||
set = _free(set);
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static unsigned hash(const char* str) {
|
||||
unsigned hash = UINT32_C(0x9e3779b9);
|
||||
const unsigned char* p = (const unsigned char*)str;
|
||||
|
||||
while (*p) {
|
||||
hash += *p++;
|
||||
hash += hash << 10;
|
||||
hash ^= hash >> 6;
|
||||
}
|
||||
|
||||
hash += hash << 3;
|
||||
hash ^= hash >> 11;
|
||||
hash += hash << 15;
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
static int compare_symbols(const void* arg1, const void* arg2) {
|
||||
const struct symbols* s1 = arg1;
|
||||
const struct symbols* s2 = arg2;
|
||||
|
||||
if (s1->hash > s2->hash) return 1;
|
||||
if (s1->hash < s2->hash) return -1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void sort_symbols(struct symbols* values, size_t count, unsigned bpp) {
|
||||
if (count < 128) {
|
||||
qsort(values, count, sizeof(*values), compare_symbols);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
struct symbols* temporary = xmalloc(count * sizeof(*temporary));
|
||||
struct symbols* source = values;
|
||||
struct symbols* destination = temporary;
|
||||
unsigned passes = (bpp + 7) / 8;
|
||||
|
||||
for (unsigned pass = 0; pass < passes; ++pass) {
|
||||
size_t offsets[256] = {0};
|
||||
unsigned shift = pass * 8;
|
||||
for (size_t i = 0; i < count; ++i) ++offsets[(source[i].hash >> shift) & 0xffu];
|
||||
|
||||
size_t position = 0;
|
||||
for (size_t i = 0; i < 256; ++i) {
|
||||
size_t bucket_count = offsets[i];
|
||||
offsets[i] = position;
|
||||
position += bucket_count;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
unsigned bucket = (source[i].hash >> shift) & 0xffu;
|
||||
destination[offsets[bucket]++] = source[i];
|
||||
}
|
||||
|
||||
struct symbols* swap = source;
|
||||
source = destination;
|
||||
destination = swap;
|
||||
}
|
||||
|
||||
if (source != values) memcpy(values, source, count * sizeof(*values));
|
||||
_free(temporary);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
static const char base64_alphabet[] =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
|
||||
static size_t base64_encoded_size(size_t byte_count) {
|
||||
if (byte_count > SIZE_MAX - 2) abort();
|
||||
size_t groups = (byte_count + 2) / 3;
|
||||
if (groups > (SIZE_MAX - FORMAT_HEADER_LEN - 1) / 4) abort();
|
||||
|
||||
return groups * 4;
|
||||
}
|
||||
|
||||
static void base64_encode(const unsigned char* input, size_t input_len, char* output) {
|
||||
while (input_len >= 3) {
|
||||
uint32_t value = ((uint32_t)input[0] << 16) | ((uint32_t)input[1] << 8) | input[2];
|
||||
output[0] = base64_alphabet[(value >> 18) & 0x3f];
|
||||
output[1] = base64_alphabet[(value >> 12) & 0x3f];
|
||||
output[2] = base64_alphabet[(value >> 6) & 0x3f];
|
||||
output[3] = base64_alphabet[value & 0x3f];
|
||||
input += 3;
|
||||
input_len -= 3;
|
||||
output += 4;
|
||||
}
|
||||
|
||||
if (input_len == 1) {
|
||||
uint32_t value = (uint32_t)input[0] << 16;
|
||||
output[0] = base64_alphabet[(value >> 18) & 0x3f];
|
||||
output[1] = base64_alphabet[(value >> 12) & 0x3f];
|
||||
output[2] = '=';
|
||||
output[3] = '=';
|
||||
} else if (input_len == 2) {
|
||||
uint32_t value = ((uint32_t)input[0] << 16) | ((uint32_t)input[1] << 8);
|
||||
output[0] = base64_alphabet[(value >> 18) & 0x3f];
|
||||
output[1] = base64_alphabet[(value >> 12) & 0x3f];
|
||||
output[2] = base64_alphabet[(value >> 6) & 0x3f];
|
||||
output[3] = '=';
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
static unsigned char* pack_hashes(const unsigned* hashes, size_t count, unsigned bpp,
|
||||
size_t* byte_count) {
|
||||
if (count > (SIZE_MAX - 7) / bpp) abort();
|
||||
size_t bit_count = count * bpp;
|
||||
*byte_count = (bit_count + 7) / 8;
|
||||
unsigned char* bytes = xmalloc(*byte_count);
|
||||
unsigned char* output = bytes;
|
||||
uint64_t bits = 0;
|
||||
unsigned filled = 0;
|
||||
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
bits |= (uint64_t)hashes[i] << filled;
|
||||
filled += bpp;
|
||||
|
||||
while (filled >= 8) {
|
||||
*output++ = (unsigned char)bits;
|
||||
bits >>= 8;
|
||||
filled -= 8;
|
||||
}
|
||||
}
|
||||
|
||||
if (filled) *output++ = (unsigned char)bits;
|
||||
assert((size_t)(output - bytes) == *byte_count);
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
const char* set_fini(struct set* set, int bpp) {
|
||||
assert(set != NULL);
|
||||
assert(set->cnt > 0);
|
||||
assert(bpp >= 10 && bpp <= 32);
|
||||
|
||||
unsigned mask = bpp < 32 ? (UINT32_C(1) << bpp) - 1 : UINT32_MAX;
|
||||
for (size_t i = 0; i < set->cnt; ++i) {
|
||||
set->symbols_v[i].hash = hash(set->strings + set->symbols_v[i].offset) & mask;
|
||||
}
|
||||
sort_symbols(set->symbols_v, set->cnt, (unsigned)bpp);
|
||||
|
||||
for (size_t i = 0; i + 1 < set->cnt; ++i) {
|
||||
if (set->symbols_v[i].hash != set->symbols_v[i + 1].hash) continue;
|
||||
const char* left = set->strings + set->symbols_v[i].offset;
|
||||
const char* right = set->strings + set->symbols_v[i + 1].offset;
|
||||
if (strcmp(left, right) != 0) fprintf(stderr, "warning: hash collision: %s %s\n", left, right);
|
||||
}
|
||||
|
||||
unsigned* unique_hashes = xmalloc(set->cnt * sizeof(*unique_hashes));
|
||||
size_t unique_count = 0;
|
||||
for (size_t i = 0; i < set->cnt; ++i) {
|
||||
while (i + 1 < set->cnt && set->symbols_v[i].hash == set->symbols_v[i + 1].hash) ++i;
|
||||
unique_hashes[unique_count++] = set->symbols_v[i].hash;
|
||||
}
|
||||
|
||||
size_t byte_count;
|
||||
unsigned char* allocated_bytes = NULL;
|
||||
const unsigned char* bytes;
|
||||
#if UINT_MAX == UINT32_MAX && defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && \
|
||||
__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
|
||||
if (bpp == 32 && sizeof(unsigned) == 4) {
|
||||
byte_count = unique_count * sizeof(*unique_hashes);
|
||||
bytes = (const unsigned char*)unique_hashes;
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
allocated_bytes = pack_hashes(unique_hashes, unique_count, (unsigned)bpp, &byte_count);
|
||||
bytes = allocated_bytes;
|
||||
}
|
||||
size_t payload_len = base64_encoded_size(byte_count);
|
||||
char* output = xmalloc(FORMAT_HEADER_LEN + payload_len + 1);
|
||||
memcpy(output, FORMAT_PREFIX, sizeof(FORMAT_PREFIX) - 1);
|
||||
output[2] = (char)('0' + bpp / 10);
|
||||
output[3] = (char)('0' + bpp % 10);
|
||||
base64_encode(bytes, byte_count, output + FORMAT_HEADER_LEN);
|
||||
output[FORMAT_HEADER_LEN + payload_len] = '\0';
|
||||
|
||||
_free(allocated_bytes);
|
||||
_free(unique_hashes);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
static const unsigned char base64_values[256] = {
|
||||
['A'] = 1, ['B'] = 2, ['C'] = 3, ['D'] = 4, ['E'] = 5, ['F'] = 6, ['G'] = 7, ['H'] = 8,
|
||||
['I'] = 9, ['J'] = 10, ['K'] = 11, ['L'] = 12, ['M'] = 13, ['N'] = 14, ['O'] = 15, ['P'] = 16,
|
||||
['Q'] = 17, ['R'] = 18, ['S'] = 19, ['T'] = 20, ['U'] = 21, ['V'] = 22, ['W'] = 23, ['X'] = 24,
|
||||
['Y'] = 25, ['Z'] = 26, ['a'] = 27, ['b'] = 28, ['c'] = 29, ['d'] = 30, ['e'] = 31, ['f'] = 32,
|
||||
['g'] = 33, ['h'] = 34, ['i'] = 35, ['j'] = 36, ['k'] = 37, ['l'] = 38, ['m'] = 39, ['n'] = 40,
|
||||
['o'] = 41, ['p'] = 42, ['q'] = 43, ['r'] = 44, ['s'] = 45, ['t'] = 46, ['u'] = 47, ['v'] = 48,
|
||||
['w'] = 49, ['x'] = 50, ['y'] = 51, ['z'] = 52, ['0'] = 53, ['1'] = 54, ['2'] = 55, ['3'] = 56,
|
||||
['4'] = 57, ['5'] = 58, ['6'] = 59, ['7'] = 60, ['8'] = 61, ['9'] = 62, ['+'] = 63, ['/'] = 64,
|
||||
};
|
||||
|
||||
static inline int base64_value(unsigned char c) { return (int)base64_values[c] - 1; }
|
||||
|
||||
static inline int has_set_prefix(const char* str) {
|
||||
return str[0] == 's' && str[1] == 'e' && str[2] == 't' && str[3] == ':';
|
||||
}
|
||||
|
||||
static int set_meta_init(const char* source, struct set_meta* meta) {
|
||||
const char* str = source;
|
||||
if (has_set_prefix(str)) str += 4;
|
||||
if (has_set_prefix(str)) return -1;
|
||||
|
||||
/* A valid direct set has at least one complete Base64 quartet. Checking
|
||||
* this fixed prefix makes cache hits independent of total key length. */
|
||||
if (str[0] != FORMAT_PREFIX[0] || str[1] != FORMAT_PREFIX[1]) return -1;
|
||||
if (str[2] < '0' || str[2] > '9' || str[3] < '0' || str[3] > '9') return -1;
|
||||
if (str[4] == '\0' || str[5] == '\0' || str[6] == '\0' || str[7] == '\0') return -1;
|
||||
|
||||
unsigned bpp = (unsigned)(str[2] - '0') * 10 + (unsigned)(str[3] - '0');
|
||||
if (bpp < 10 || bpp > 32) return -1;
|
||||
|
||||
meta->str = str;
|
||||
meta->len = 0;
|
||||
meta->bpp = bpp;
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct decode_writer {
|
||||
unsigned* hashes;
|
||||
size_t capacity;
|
||||
size_t written;
|
||||
uint64_t bits;
|
||||
uint64_t mask;
|
||||
unsigned previous;
|
||||
unsigned filled;
|
||||
unsigned bpp;
|
||||
int has_previous;
|
||||
};
|
||||
|
||||
static inline int decode_writer_put(struct decode_writer* writer, uint32_t bytes,
|
||||
unsigned byte_count) {
|
||||
writer->bits |= (uint64_t)bytes << writer->filled;
|
||||
writer->filled += byte_count * 8;
|
||||
|
||||
while (writer->filled >= writer->bpp) {
|
||||
if (writer->written == writer->capacity) return -1;
|
||||
|
||||
unsigned current = (unsigned)(writer->bits & writer->mask);
|
||||
writer->bits >>= writer->bpp;
|
||||
writer->filled -= writer->bpp;
|
||||
|
||||
if (writer->has_previous && writer->previous >= current) return -1;
|
||||
if (writer->hashes) writer->hashes[writer->written] = current;
|
||||
writer->previous = current;
|
||||
writer->has_previous = 1;
|
||||
++writer->written;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int decode_base64_bytes(const char* input, size_t input_len, unsigned char* output,
|
||||
size_t output_len) {
|
||||
unsigned char* const output_end = output + output_len;
|
||||
size_t offset = 0;
|
||||
for (; offset + 4 < input_len; offset += 4) {
|
||||
int v0 = base64_value((unsigned char)input[offset]);
|
||||
int v1 = base64_value((unsigned char)input[offset + 1]);
|
||||
int v2 = base64_value((unsigned char)input[offset + 2]);
|
||||
int v3 = base64_value((unsigned char)input[offset + 3]);
|
||||
if ((v0 | v1 | v2 | v3) < 0) return -1;
|
||||
output[0] = (unsigned char)((v0 << 2) | (v1 >> 4));
|
||||
output[1] = (unsigned char)(((v1 & 0x0f) << 4) | (v2 >> 2));
|
||||
output[2] = (unsigned char)(((v2 & 0x03) << 6) | v3);
|
||||
output += 3;
|
||||
}
|
||||
|
||||
int v0 = base64_value((unsigned char)input[offset]);
|
||||
int v1 = base64_value((unsigned char)input[offset + 1]);
|
||||
if ((v0 | v1) < 0 || output == output_end) return -1;
|
||||
*output++ = (unsigned char)((v0 << 2) | (v1 >> 4));
|
||||
|
||||
if (input[offset + 2] == '=') {
|
||||
return input[offset + 3] == '=' && (v1 & 0x0f) == 0 && output == output_end ? 0 : -1;
|
||||
}
|
||||
|
||||
int v2 = base64_value((unsigned char)input[offset + 2]);
|
||||
if (v2 < 0 || output == output_end) return -1;
|
||||
*output++ = (unsigned char)(((v1 & 0x0f) << 4) | (v2 >> 2));
|
||||
if (input[offset + 3] == '=') return (v2 & 0x03) == 0 && output == output_end ? 0 : -1;
|
||||
|
||||
int v3 = base64_value((unsigned char)input[offset + 3]);
|
||||
if (v3 < 0 || output == output_end) return -1;
|
||||
*output++ = (unsigned char)(((v2 & 0x03) << 6) | v3);
|
||||
return output == output_end ? 0 : -1;
|
||||
}
|
||||
|
||||
static inline int decode_base64_triplet(const char* input, uint32_t* triplet) {
|
||||
int v0 = base64_value((unsigned char)input[0]);
|
||||
int v1 = base64_value((unsigned char)input[1]);
|
||||
int v2 = base64_value((unsigned char)input[2]);
|
||||
int v3 = base64_value((unsigned char)input[3]);
|
||||
if ((v0 | v1 | v2 | v3) < 0) return -1;
|
||||
|
||||
*triplet = (uint32_t)((v0 << 2) | (v1 >> 4)) |
|
||||
(uint32_t)(((v1 & 0x0f) << 4) | (v2 >> 2)) << 8 |
|
||||
(uint32_t)(((v2 & 0x03) << 6) | v3) << 16;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int decode_base64_u32(const char* input, size_t input_len, unsigned* hashes,
|
||||
size_t count) {
|
||||
size_t blocks = count / 3;
|
||||
size_t written = 0;
|
||||
unsigned previous = 0;
|
||||
int has_previous = 0;
|
||||
|
||||
for (size_t block = 0; block < blocks; ++block) {
|
||||
uint32_t t0, t1, t2, t3;
|
||||
if (decode_base64_triplet(input, &t0) < 0 ||
|
||||
decode_base64_triplet(input + 4, &t1) < 0 ||
|
||||
decode_base64_triplet(input + 8, &t2) < 0 ||
|
||||
decode_base64_triplet(input + 12, &t3) < 0)
|
||||
return -1;
|
||||
|
||||
unsigned value0 = (unsigned)(t0 | ((t1 & UINT32_C(0xff)) << 24));
|
||||
unsigned value1 = (unsigned)((t1 >> 8) | ((t2 & UINT32_C(0xffff)) << 16));
|
||||
unsigned value2 = (unsigned)((t2 >> 16) | (t3 << 8));
|
||||
if ((has_previous && previous >= value0) || value0 >= value1 || value1 >= value2) return -1;
|
||||
|
||||
hashes[written++] = value0;
|
||||
hashes[written++] = value1;
|
||||
hashes[written++] = value2;
|
||||
previous = value2;
|
||||
has_previous = 1;
|
||||
input += 16;
|
||||
input_len -= 16;
|
||||
}
|
||||
|
||||
size_t remaining = count - written;
|
||||
if (remaining != 0) {
|
||||
size_t byte_count = remaining * sizeof(*hashes);
|
||||
unsigned char tail[2 * sizeof(*hashes)];
|
||||
if (decode_base64_bytes(input, input_len, tail, byte_count) < 0) return -1;
|
||||
for (size_t i = 0; i < remaining; ++i, ++written) {
|
||||
const unsigned char* bytes = tail + i * 4;
|
||||
unsigned current = (unsigned)bytes[0] | ((unsigned)bytes[1] << 8) |
|
||||
((unsigned)bytes[2] << 16) | ((unsigned)bytes[3] << 24);
|
||||
if (has_previous && previous >= current) return -1;
|
||||
hashes[written] = current;
|
||||
previous = current;
|
||||
has_previous = 1;
|
||||
}
|
||||
} else if (input_len != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int decode_set_sized(const char* str, size_t str_len, struct decoded_set* decoded) {
|
||||
if (str_len == 0) str_len = strlen(str);
|
||||
if (str_len <= FORMAT_HEADER_LEN) return -1;
|
||||
if (strncmp(str, FORMAT_PREFIX, sizeof(FORMAT_PREFIX) - 1) != 0) return -1;
|
||||
if (str[2] < '0' || str[2] > '9' || str[3] < '0' || str[3] > '9') return -1;
|
||||
|
||||
unsigned bpp = (unsigned)(str[2] - '0') * 10 + (unsigned)(str[3] - '0');
|
||||
if (bpp < 10 || bpp > 32) return -1;
|
||||
|
||||
const char* input = str + FORMAT_HEADER_LEN;
|
||||
size_t input_len = str_len - FORMAT_HEADER_LEN;
|
||||
if (input_len == 0 || input_len % 4 != 0 || input_len / 4 > SIZE_MAX / 3) return -1;
|
||||
|
||||
size_t byte_count = input_len / 4 * 3;
|
||||
if (input[input_len - 1] == '=') --byte_count;
|
||||
if (input[input_len - 2] == '=') --byte_count;
|
||||
if (byte_count > SIZE_MAX / 8) return -1;
|
||||
|
||||
size_t count = byte_count * 8 / bpp;
|
||||
if (count == 0 || count > (SIZE_MAX - 7) / bpp || count > SIZE_MAX / sizeof(unsigned) ||
|
||||
(count * bpp + 7) / 8 != byte_count) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
unsigned* hashes = xmalloc(count * sizeof(*hashes));
|
||||
#if UINT_MAX == UINT32_MAX
|
||||
if (bpp == 32 && sizeof(unsigned) == 4) {
|
||||
if (decode_base64_u32(input, input_len, hashes, count) < 0) goto invalid;
|
||||
if (decoded) {
|
||||
decoded->hashes = hashes;
|
||||
decoded->count = count;
|
||||
decoded->bpp = bpp;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Byte-aligned widths can bypass the generic bit reservoir. Decode the
|
||||
* Base64 payload into the front of the final allocation, then expand 16-
|
||||
* and 24-bit values backwards so unread packed bytes are never overwritten. */
|
||||
if ((bpp == 16 || bpp == 24) && sizeof(unsigned) == 4) {
|
||||
if (decode_base64_bytes(input, input_len, (unsigned char*)hashes, byte_count) < 0) goto invalid;
|
||||
if (bpp == 16) {
|
||||
for (size_t i = count; i > 0; --i) {
|
||||
const unsigned char* bytes = (const unsigned char*)hashes + (i - 1) * 2;
|
||||
hashes[i - 1] = (unsigned)bytes[0] | ((unsigned)bytes[1] << 8);
|
||||
}
|
||||
} else if (bpp == 24) {
|
||||
for (size_t i = count; i > 0; --i) {
|
||||
const unsigned char* bytes = (const unsigned char*)hashes + (i - 1) * 3;
|
||||
hashes[i - 1] = (unsigned)bytes[0] | ((unsigned)bytes[1] << 8) |
|
||||
((unsigned)bytes[2] << 16);
|
||||
}
|
||||
}
|
||||
for (size_t i = 1; i < count; ++i) {
|
||||
if (hashes[i - 1] >= hashes[i]) goto invalid;
|
||||
}
|
||||
if (decoded) {
|
||||
decoded->hashes = hashes;
|
||||
decoded->count = count;
|
||||
decoded->bpp = bpp;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
struct decode_writer writer = {
|
||||
.hashes = hashes,
|
||||
.capacity = count,
|
||||
.mask = bpp < 32 ? (UINT64_C(1) << bpp) - 1 : UINT32_MAX,
|
||||
.bpp = bpp,
|
||||
};
|
||||
|
||||
for (size_t offset = 0; offset < input_len; offset += 4) {
|
||||
int v0 = base64_value((unsigned char)input[offset]);
|
||||
int v1 = base64_value((unsigned char)input[offset + 1]);
|
||||
int last = offset + 4 == input_len;
|
||||
if (v0 < 0 || v1 < 0) goto invalid;
|
||||
|
||||
uint32_t bytes = (uint32_t)((v0 << 2) | (v1 >> 4));
|
||||
if (input[offset + 2] == '=') {
|
||||
if (!last || input[offset + 3] != '=' || (v1 & 0x0f) != 0 ||
|
||||
decode_writer_put(&writer, bytes, 1) < 0)
|
||||
goto invalid;
|
||||
continue;
|
||||
}
|
||||
|
||||
int v2 = base64_value((unsigned char)input[offset + 2]);
|
||||
if (v2 < 0) goto invalid;
|
||||
bytes |= (uint32_t)(((v1 & 0x0f) << 4) | (v2 >> 2)) << 8;
|
||||
if (input[offset + 3] == '=') {
|
||||
if (!last || (v2 & 0x03) != 0 || decode_writer_put(&writer, bytes, 2) < 0) goto invalid;
|
||||
continue;
|
||||
}
|
||||
|
||||
int v3 = base64_value((unsigned char)input[offset + 3]);
|
||||
if (v3 < 0) goto invalid;
|
||||
bytes |= (uint32_t)(((v2 & 0x03) << 6) | v3) << 16;
|
||||
if (decode_writer_put(&writer, bytes, 3) < 0) goto invalid;
|
||||
}
|
||||
|
||||
if (writer.written != count || writer.bits != 0) goto invalid;
|
||||
|
||||
if (decoded) {
|
||||
decoded->hashes = hashes;
|
||||
decoded->count = count;
|
||||
decoded->bpp = bpp;
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
invalid:
|
||||
_free(hashes);
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Reduce a sorted set of (bpp + 1)-bit values to a sorted set of bpp-bit values. */
|
||||
static size_t downsample_set(size_t count, const unsigned* hashes, unsigned* result, unsigned bpp) {
|
||||
unsigned mask = (UINT32_C(1) << bpp) - 1;
|
||||
size_t lower = 0;
|
||||
size_t upper = count;
|
||||
|
||||
while (lower < upper) {
|
||||
size_t middle = lower + (upper - lower) / 2;
|
||||
if (hashes[middle] <= mask)
|
||||
lower = middle + 1;
|
||||
else
|
||||
upper = middle;
|
||||
}
|
||||
|
||||
unsigned* output = result;
|
||||
const unsigned* low = hashes;
|
||||
const unsigned* low_end = hashes + lower;
|
||||
const unsigned* high = hashes + lower;
|
||||
const unsigned* high_end = hashes + count;
|
||||
|
||||
while (low < low_end && high < high_end) {
|
||||
unsigned low_value = *low;
|
||||
unsigned high_value = *high & mask;
|
||||
if (low_value < high_value) {
|
||||
*output++ = low_value;
|
||||
++low;
|
||||
} else if (high_value < low_value) {
|
||||
*output++ = high_value;
|
||||
++high;
|
||||
} else {
|
||||
*output++ = low_value;
|
||||
++low;
|
||||
++high;
|
||||
}
|
||||
}
|
||||
while (low < low_end) *output++ = *low++;
|
||||
while (high < high_end) *output++ = *high++ & mask;
|
||||
|
||||
return (size_t)(output - result);
|
||||
}
|
||||
|
||||
static void downsample_radix_to(struct decoded_set* set, unsigned target_bpp) {
|
||||
unsigned* original = set->hashes;
|
||||
unsigned* scratch = xmalloc(set->count * sizeof(*scratch));
|
||||
unsigned mask = (UINT32_C(1) << target_bpp) - 1;
|
||||
for (size_t i = 0; i < set->count; ++i) original[i] &= mask;
|
||||
|
||||
unsigned* source = original;
|
||||
unsigned* destination = scratch;
|
||||
unsigned passes = (target_bpp + 7) / 8;
|
||||
for (unsigned pass = 0; pass < passes; ++pass) {
|
||||
size_t offsets[256] = {0};
|
||||
unsigned shift = pass * 8;
|
||||
for (size_t i = 0; i < set->count; ++i) ++offsets[(source[i] >> shift) & 0xffu];
|
||||
|
||||
size_t position = 0;
|
||||
for (size_t i = 0; i < 256; ++i) {
|
||||
size_t bucket_count = offsets[i];
|
||||
offsets[i] = position;
|
||||
position += bucket_count;
|
||||
}
|
||||
for (size_t i = 0; i < set->count; ++i) {
|
||||
unsigned value = source[i];
|
||||
destination[offsets[(value >> shift) & 0xffu]++] = value;
|
||||
}
|
||||
|
||||
unsigned* swap = source;
|
||||
source = destination;
|
||||
destination = swap;
|
||||
}
|
||||
|
||||
size_t unique_count = 1;
|
||||
for (size_t i = 1; i < set->count; ++i) {
|
||||
if (source[i] != source[unique_count - 1]) source[unique_count++] = source[i];
|
||||
}
|
||||
|
||||
if (source == original) {
|
||||
_free(scratch);
|
||||
} else {
|
||||
_free(original);
|
||||
set->hashes = scratch;
|
||||
}
|
||||
set->count = unique_count;
|
||||
set->bpp = target_bpp;
|
||||
}
|
||||
|
||||
static void downsample_to(struct decoded_set* set, unsigned target_bpp) {
|
||||
if (set->bpp == target_bpp) return;
|
||||
|
||||
unsigned passes = (target_bpp + 7) / 8;
|
||||
if (set->bpp - target_bpp > passes) {
|
||||
downsample_radix_to(set, target_bpp);
|
||||
return;
|
||||
}
|
||||
|
||||
unsigned* original = set->hashes;
|
||||
unsigned* scratch = xmalloc(set->count * sizeof(*scratch));
|
||||
unsigned* source = original;
|
||||
unsigned* destination = scratch;
|
||||
|
||||
while (set->bpp > target_bpp) {
|
||||
--set->bpp;
|
||||
set->count = downsample_set(set->count, source, destination, set->bpp);
|
||||
unsigned* swap = source;
|
||||
source = destination;
|
||||
destination = swap;
|
||||
}
|
||||
|
||||
if (source == original) {
|
||||
_free(scratch);
|
||||
} else {
|
||||
_free(original);
|
||||
set->hashes = scratch;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
static uint32_t decoded_cache_fingerprint(const struct set_meta* meta, unsigned target_bpp) {
|
||||
const unsigned char* str = (const unsigned char*)meta->str;
|
||||
uint32_t fingerprint = (uint32_t)str[4] | ((uint32_t)str[5] << 8) |
|
||||
((uint32_t)str[6] << 16) | ((uint32_t)str[7] << 24);
|
||||
fingerprint ^= meta->bpp * UINT32_C(0x27d4eb2d);
|
||||
fingerprint ^= target_bpp * UINT32_C(0x85ebca6b);
|
||||
fingerprint ^= fingerprint >> 11;
|
||||
fingerprint *= UINT32_C(0x9e3779b1);
|
||||
fingerprint ^= fingerprint >> 16;
|
||||
return fingerprint;
|
||||
}
|
||||
|
||||
static void decoded_cache_touch(struct decoded_cache_entry* entry, unsigned cache_id) {
|
||||
if (entry == decoded_cache_newest[cache_id]) return;
|
||||
|
||||
if (entry->newer) entry->newer->older = entry->older;
|
||||
if (entry->older) entry->older->newer = entry->newer;
|
||||
if (entry == decoded_cache_oldest[cache_id]) decoded_cache_oldest[cache_id] = entry->newer;
|
||||
|
||||
entry->newer = NULL;
|
||||
entry->older = decoded_cache_newest[cache_id];
|
||||
decoded_cache_newest[cache_id]->newer = entry;
|
||||
decoded_cache_newest[cache_id] = entry;
|
||||
}
|
||||
|
||||
static void decoded_cache_remove(struct decoded_cache_entry* victim, unsigned cache_id) {
|
||||
if (victim->newer)
|
||||
victim->newer->older = victim->older;
|
||||
else
|
||||
decoded_cache_newest[cache_id] = victim->older;
|
||||
if (victim->older)
|
||||
victim->older->newer = victim->newer;
|
||||
else
|
||||
decoded_cache_oldest[cache_id] = victim->newer;
|
||||
|
||||
struct decoded_cache_entry** link = &decoded_cache_buckets[cache_id][victim->bucket];
|
||||
while (*link && *link != victim) link = &(*link)->bucket_next;
|
||||
assert(*link == victim);
|
||||
*link = victim->bucket_next;
|
||||
assert(decoded_cache_count[cache_id] > 0);
|
||||
--decoded_cache_count[cache_id];
|
||||
|
||||
_free(victim->hashes);
|
||||
_free(victim);
|
||||
}
|
||||
|
||||
static void decoded_cache_reset_pair_identities(void) {
|
||||
for (unsigned bucket = 0; bucket < DECODED_CACHE_BUCKETS; ++bucket) {
|
||||
for (struct decoded_cache_entry* provider = decoded_cache_buckets[0][bucket]; provider;
|
||||
provider = provider->bucket_next) {
|
||||
for (unsigned i = 0; i < PAIR_CACHE_SIZE; ++i) provider->pairs[i].other_identity = 0;
|
||||
}
|
||||
}
|
||||
decoded_cache_next_identity = 1;
|
||||
}
|
||||
|
||||
static int cache_decode_set(const struct set_meta* meta, unsigned target_bpp, unsigned cache_id,
|
||||
const unsigned** hashes, size_t* count,
|
||||
struct decoded_cache_entry** cache_entry) {
|
||||
assert(cache_id < 2);
|
||||
assert(target_bpp <= meta->bpp);
|
||||
|
||||
uint32_t fingerprint = decoded_cache_fingerprint(meta, target_bpp);
|
||||
unsigned bucket = fingerprint & (DECODED_CACHE_BUCKETS - 1);
|
||||
for (struct decoded_cache_entry* entry = decoded_cache_buckets[cache_id][bucket]; entry;
|
||||
entry = entry->bucket_next) {
|
||||
if (entry->fingerprint != fingerprint || entry->target_bpp != target_bpp ||
|
||||
strcmp(entry->str, meta->str) != 0)
|
||||
continue;
|
||||
|
||||
decoded_cache_touch(entry, cache_id);
|
||||
*hashes = entry->hashes;
|
||||
*count = entry->count;
|
||||
*cache_entry = entry;
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t len = strlen(meta->str);
|
||||
size_t input_len = len - FORMAT_HEADER_LEN;
|
||||
if (input_len == 0 || input_len % 4 != 0 || input_len / 4 > SIZE_MAX / 3) return -1;
|
||||
|
||||
struct decoded_set decoded;
|
||||
if (decode_set_sized(meta->str, len, &decoded) < 0) return -1;
|
||||
if (decoded.bpp != meta->bpp) {
|
||||
_free(decoded.hashes);
|
||||
return -1;
|
||||
}
|
||||
downsample_to(&decoded, target_bpp);
|
||||
|
||||
if (len > SIZE_MAX - sizeof(struct decoded_cache_entry) - 1) {
|
||||
_free(decoded.hashes);
|
||||
return -1;
|
||||
}
|
||||
struct decoded_cache_entry* entry = xmalloc(sizeof(*entry) + len + 1);
|
||||
memset(entry, 0, sizeof(*entry));
|
||||
entry->str = (char*)(entry + 1);
|
||||
memcpy(entry->str, meta->str, len + 1);
|
||||
entry->hashes = decoded.hashes;
|
||||
entry->len = len;
|
||||
entry->count = decoded.count;
|
||||
entry->fingerprint = fingerprint;
|
||||
if (decoded_cache_next_identity == 0) decoded_cache_reset_pair_identities();
|
||||
entry->identity = decoded_cache_next_identity++;
|
||||
entry->bucket = bucket;
|
||||
entry->target_bpp = target_bpp;
|
||||
|
||||
if (decoded_cache_count[cache_id] == DECODED_CACHE_SIZE) {
|
||||
decoded_cache_remove(decoded_cache_oldest[cache_id], cache_id);
|
||||
}
|
||||
|
||||
entry->bucket_next = decoded_cache_buckets[cache_id][bucket];
|
||||
decoded_cache_buckets[cache_id][bucket] = entry;
|
||||
entry->older = decoded_cache_newest[cache_id];
|
||||
if (decoded_cache_newest[cache_id]) {
|
||||
decoded_cache_newest[cache_id]->newer = entry;
|
||||
} else {
|
||||
decoded_cache_oldest[cache_id] = entry;
|
||||
}
|
||||
decoded_cache_newest[cache_id] = entry;
|
||||
++decoded_cache_count[cache_id];
|
||||
|
||||
*hashes = entry->hashes;
|
||||
*count = entry->count;
|
||||
*cache_entry = entry;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const unsigned* step_lower_bound(const unsigned* first, const unsigned* last, unsigned value,
|
||||
size_t jump) {
|
||||
size_t count = (size_t)(last - first);
|
||||
if (count == 0 || first[0] >= value) return first;
|
||||
if (jump == 0) jump = 1;
|
||||
|
||||
size_t position = 0;
|
||||
size_t step = jump;
|
||||
while (step != 0) {
|
||||
if (step > count - position - 1) {
|
||||
step /= 2;
|
||||
continue;
|
||||
}
|
||||
size_t next = position + step;
|
||||
if (first[next] < value)
|
||||
position = next;
|
||||
else
|
||||
step /= 2;
|
||||
}
|
||||
|
||||
return first + position + 1;
|
||||
}
|
||||
|
||||
static int sorted_subset(const unsigned* small, size_t small_count, const unsigned* large,
|
||||
size_t large_count) {
|
||||
const unsigned* small_end = small + small_count;
|
||||
const unsigned* large_end = large + large_count;
|
||||
size_t jump = large_count / small_count;
|
||||
|
||||
if (jump < 4) {
|
||||
while (small < small_end) {
|
||||
unsigned value = *small++;
|
||||
while (large < large_end && *large < value) ++large;
|
||||
if (large == large_end || *large != value) return 0;
|
||||
++large;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
while (small < small_end) {
|
||||
unsigned value = *small++;
|
||||
large = step_lower_bound(large, large_end, value, jump);
|
||||
if (large == large_end || *large != value) return 0;
|
||||
++large;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int rpmsetcmp_locked(const char* str1, const char* str2) {
|
||||
struct set_meta meta1;
|
||||
if (set_meta_init(str1, &meta1) < 0) return -3;
|
||||
|
||||
struct set_meta meta2;
|
||||
int meta2_status = set_meta_init(str2, &meta2);
|
||||
unsigned target_bpp =
|
||||
meta2_status == 0 && meta2.bpp < meta1.bpp ? meta2.bpp : meta1.bpp;
|
||||
|
||||
const unsigned* hashes1;
|
||||
size_t count1;
|
||||
struct decoded_cache_entry* entry1;
|
||||
if (cache_decode_set(&meta1, target_bpp, 0, &hashes1, &count1, &entry1) < 0) return -3;
|
||||
if (meta2_status < 0) return -4;
|
||||
|
||||
const unsigned* hashes2;
|
||||
size_t count2;
|
||||
struct decoded_cache_entry* entry2;
|
||||
if (cache_decode_set(&meta2, target_bpp, 1, &hashes2, &count2, &entry2) < 0) return -4;
|
||||
|
||||
for (unsigned i = 0; i < PAIR_CACHE_SIZE; ++i) {
|
||||
if (entry1->pairs[i].other_identity == entry2->identity) return entry1->pairs[i].result;
|
||||
}
|
||||
|
||||
int result;
|
||||
if (count1 == count2)
|
||||
result = memcmp(hashes1, hashes2, count1 * sizeof(*hashes1)) == 0 ? 0 : -2;
|
||||
else if (count1 > count2)
|
||||
result = sorted_subset(hashes2, count2, hashes1, count1) ? 1 : -2;
|
||||
else
|
||||
result = sorted_subset(hashes1, count1, hashes2, count2) ? -1 : -2;
|
||||
|
||||
struct pair_cache_entry* pair = &entry1->pairs[entry1->pair_next++ % PAIR_CACHE_SIZE];
|
||||
pair->other_identity = entry2->identity;
|
||||
pair->result = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
int rpmsetcmp(const char* str1, const char* str2) {
|
||||
while (atomic_flag_test_and_set_explicit(&decoded_cache_lock, memory_order_acquire)) {
|
||||
}
|
||||
int result = rpmsetcmp_locked(str1, str2);
|
||||
atomic_flag_clear_explicit(&decoded_cache_lock, memory_order_release);
|
||||
return result;
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
## хранить сразу расшифрованный set
|
||||
|
||||
- WIP [здесь](new_version/direct_hash/about.md)
|
||||
- огромная часть ресурсов уходит не на просмотр включения множеств, а на декодирование set-строк
|
||||
- при возможности хранить больше данных за более дешёвое сравнение - прекрасно
|
||||
- тупо условный формат:
|
||||
@@ -37,7 +38,3 @@
|
||||
|
||||
- если условно "отсортировать" массив provides/requires, можно получить лучшую работу с кжшом
|
||||
- (надеюсь, что под капотом оно уже и так это делает, но проверить стоит)
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user