Goblin Core Performance Brief

This document is an overview of Goblin Core's architecture and performance characteristics: how sorted sets are stored, where the memory and throughput wins over Redis come from, and how the benchmarks are run.

Project Goal

Goblin Core is a source-only C++23 Redis-like server from Goblin Reactor. The long-term goal is a Redis-compatible server with better memory and performance characteristics for cost-sensitive deployments. The current implementation is intentionally narrow: sorted sets plus PING, with no persistence, replication, cluster mode, Lua, transactions, modules, or general Redis key types.

The design bias is to replace pointer-heavy Redis data structures with compact, vector-oriented layouts that are friendly to cache locality and compiler auto-vectorization. SIMD-specific code should be expressed as portable C++ when possible.

Supported Command Surface

Plus Goblin-specific maintenance and persistence commands:

The server accepts RESP array commands and a basic inline command format used by tests.

Current Architecture

Zsets are not skiplists. Members are stored in packed storage and referenced by member id. Score order uses a block-oriented sorted index inspired by Python SortedContainers: inserts/removes touch compact vectors and blocks rather than skiplist nodes. Lookups use a compact member index instead of general-purpose node allocation.

Tunables

The member index is a Swiss table sized to a whole number of 16-byte groups (not the next power of two) and bucketed with a multiply-shift reduction, so its load factor stays near 31/32 at any member count rather than dropping to ~50% just past a power of two. GOBLIN.OPTIMIZE key [density] repacks it (and the score index) to a target load factor; the default 0.97 is tight while leaving empty slots so absent-member lookups terminate quickly. Density 1.0 minimizes memory but, with no empty slot, makes a lookup of a missing member scan the whole table — reserve it for read-only sets queried only for present members.

Current Benchmark Snapshot

Memory is the story. After a load-then-GOBLIN.OPTIMIZE sequence (the deployment path), Goblin Core holds a sorted set in roughly 51 RSS bytes per member versus about 78–83 for Redis 8.8, 84–85 for Valkey 9.1, and 103–110 for Redis 7.2.4 — roughly half the resident set of legacy Redis. Dragonfly is the nearest competitor at ~55 B/member, with Goblin Core still about 6–7% lower. Throughput is a secondary benefit; Goblin Core also leads the supported sorted-set operations on one core.

Snapshot host:

Memory (the headline)

Resident-set (RSS) delta over baseline:

Members Goblin Redis 7.2.4 Redis 8.8 Valkey 9.1 Dragonfly
250K 52.9 103.8 79.2 85.0 56.8
500K 52.0 107.7 82.8 84.9 55.5
1M 51.4 109.7 82.7 84.5 55.1
2M 51.2 106.5 80.4 84.4 54.8
4M 51.0 103.2 78.2 84.3 54.6

Goblin Core's internally tracked zset allocation is 48.4 B/member via GOBLIN.MEMORY; the remaining RSS gap is allocator and process overhead. Members are stored in a packed arena referenced by a struct-of-arrays entry (u32 offset + u16 length + score), which caps a single member at 64 KiB — far above any realistic sorted-set member.

The memory sweep uses the scattered 32-bit integer score generator from benchmarks/zset_memory.py: values are in [0, 2^32), and roughly half exceed signed i32, so Goblin Core stores this dataset at f64 score width. These are not narrow-score-only numbers.

Throughput (secondary)

Measured with redis-benchmark (a C load generator), one connection, pipeline depth 16, 1M-member keyspace. The score field is __rand_int__ with -r 1000000, so this sweep exercises Goblin Core's signed i32 score width.

Operation Goblin Redis 7.2.4 Redis 8.8 Valkey 9.1 Dragonfly
ZADD 392K 235K 232K 235K 251K
ZSCORE 581K 480K 470K 485K 406K
ZRANK 476K 332K 340K 353K 320K
ZRANGE (16) 433K 360K 365K 339K 257K
ZSCORE depth-1 latency 21.3µs 21.9µs 23.0µs 22.4µs 22.9µs

Methodology note on read throughput: single-member read throughput must be driven by a C load generator. A single Python pipelined connection is client-bound near ~350K ops/sec, so the Python harness measures the client, not the server, and reports both systems as roughly equal. That is why an earlier snapshot showed ZSCORE at 0.95x — a client-bound artifact, not a server result. The current sorted-set speed harness uses redis-benchmark; memory is measured from process RSS and was never affected.

The in-process microbench report was generated on the local macOS arm64 development machine. Treat these values as local baselines, not cross-machine targets:

Read path (--members 1000000, rank cache off, integer scores):

Write path (--members 100000, rank cache off, integer scores; steady-state remove benches zrem then immediately zadd-restore):

Older read-path baselines from the 1M-member report (still valid for rank-cache comparisons):

Read the generated reports for the full tables:

Known Performance Questions

Reproduction Commands

Build and test:

cmake -S . -B build-release -DCMAKE_BUILD_TYPE=Release
cmake --build build-release
ctest --test-dir build-release --output-on-failure

Full Redis comparison:

python3 benchmarks/run_benchmarks.py \
  --redis-server /path/to/redis-server \
  --report BENCHMARKS.md \
  --name redis-goblin-core-1m-modes \
  --latency-samples 10000

Reproduce the memory and redis-benchmark throughput snapshot above (uses a C load generator, pins the server and client to separate cores, and prints RSS bytes per member plus per-op ratios):

python3 benchmarks/redis_benchmark_speed.py \
  --goblin-bin build-release/goblin-core \
  --redis-server "$(command -v redis-server)" \
  --members 1000000 --requests 2000000 --rounds 3 \
  --server-cpu 0 --client-cpu 1

Fast CI-style smoke:

scripts/benchmark_smoke.sh

In-process microbenchmark:

cmake --build build-release --target goblin_core_microbench
./build-release/goblin_core_microbench \
  --members 1000000 \
  --ops 1000000 \
  --score-shape integer \
  --format json \
  --output benchmark-results/microbench.json

# Write path only (smaller fixture; destructive benches reset state per run):
./build-release/goblin_core_microbench \
  --members 100000 \
  --ops 100000 \
  --category write_path \
  --format json \
  --output benchmark-results/microbench-write-path.json

Design Principles

Changes keep the supported Redis subset correct and measurable. A data-structure change identifies which benchmark metric it should improve, which memory metric might regress, and which test or benchmark validates it. Architecture-specific intrinsics are avoided unless the same idea cannot be expressed in portable C++ that compilers can auto-vectorize.