English static mirror for SEO/GEO · AI-assisted translation · Read Chinese original

Typhon: A Microsecond-Latency ACID Database Engine in C#, Inspired by Game Engines

Forum topic · 小凯 · 2026-04-25

Summary

Typhon is an embedded, persistent, ACID-compliant database engine written in C#/.NET, targeting 1-2µs transaction latency for game servers and real-time simulations. Built by Loïc Baumann, a developer with 30 years of real-time 3D engine experience, Typhon borrows storage architecture from game engines: Entity Component System (ECS) concepts like archetypes, columnar cluster storage, and entity-as-ID map directly to relational database ideas like tables, columns, and rows. Key techniques include zero-allocation hot paths (ref struct, stackalloc, Pinned Object Heap, ArrayPool), Roslyn analyzers replacing Rust-style borrow checkers, per-component MVCC instead of whole-row versioning, three storage modes (Versioned/SingleVersion/Transient), zone maps yielding up to 268x speedups on range queries, and 256-byte B+Tree nodes aligned with CPU prefetcher behavior. Benchmark results include 2.5ns per-entity cluster iteration, 2.95µs full CRUD transaction lifecycle, and 7.1x parallel scaling on 8 workers—C/Rust-level performance from a managed language, demonstrating that memory layout, not language choice, is the real bottleneck.

A veteran with 30 years of real-time 3D engine experience decided to write an embedded ACID database engine in C#. Target: 1-2µs transaction latency. Method: steal storage architecture from game engines. Result: C/Rust-level performance numbers on .NET.

What Is Typhon?

Typhon is an embedded, persistent, ACID-compliant database engine written in .NET (C#), designed for game servers and real-time simulation. Its core positioning: an engine that stores data the way game engines do, while providing database-grade guarantees.

The author, Loïc Baumann (Nockawa), has 30 years of experience in real-time 3D engines and systems software. His blog series "A Database That Thinks Like a Game Engine" has published three parts so far; this post synthesizes them.

Blog series:

  • Why I'm Building a Database Engine in C#
  • What Game Engines Know About Data That Databases Forgot
  • Microsecond Latency in a Managed Language
  • GitHub: https://github.com/nockawa/typhon (to be confirmed)

    ---

    Part 1: Why C#?

    "Modern C# Is Two Languages"

    Most people only know the managed half—classes, GC, LINQ, async/await. But the other half—unsafe, fixed, ref struct, Span<T>, StructLayout(Explicit), System.Runtime.Intrinsics—is essentially C-level systems programming. JIT-generated machine code is identical to what a C compiler produces.

    The author's core thesis: the bottleneck is memory layout, not the language.

    > A cache miss to DRAM costs 61-73ns (~250 CPU cycles). A CAS hitting L1 costs 1.4ns. The ratio is 50:1.

    If your data structures are cache-friendly, the language barely matters; if cache-unfriendly, Rust's zero-cost abstractions won't save you.

    Roslyn Analyzers Instead of a Borrow Checker

    Rather than pursuing Rust's full safety guarantees, the author wrote custom compiler analyzers (TYPHON001-007) performing domain-specific safety checks only on performance-critical types. For example, ChunkAccessor must be passed by ref, Transaction must be disposed. Error messages carry domain semantics: "causes page cache deadlock" is more instructive than Rust's "value moved here."

    GC Isn't the Problem—Allocation Is

    GC pause frequency depends on allocation frequency, not the language itself. Typhon's strategy: zero allocation on hot paths. Ref structs, stackalloc, Pinned Object Heap, ArrayPool—a four-layer strategy ensures GC almost never triggers during steady-state operation.

    Part 2: What Game Engines Know That Databases Forgot

    ECS and Relational Databases Are the Same Thing

    The series' most striking insight, captured in a comparison table:

    | ECS Concept | Database Concept | Shared Principle | |-------------|------------------|------------------| | Archetype | Table | Homogeneous, fixed-schema storage | | Component | Column | Typed, batch-iterable data | | Entity | Row | Identity with dynamic composition | | System | Query | Processing all records matching a signature | | Frame Budget (16ms) | Latency SLA | Hard real-time deadline |

    Two fields, separated by decades and industry boundaries, converged on structurally identical solutions—because they solve the same fundamental problem: managing structured data under performance constraints.

    Three Things Learned from Game Engines

    1. Cache locality by default. Row stores loading all player positions drag along entire rows—names, inventory, health, mostly wasted bytes. ECS stores by type: all positions contiguous, all health values contiguous. Reading 10,000 positions is a linear memory scan where every byte is useful.

    2. Zero-copy as default behavior, not an optimization. Traditional databases deserialize records from storage pages into language objects. In ECS, components already exist in final layout in memory—you just return a pointer. Typhon preserves this: components are blittable unmanaged structs read directly from pinned memory pages.

    3. Entities are pure identity. An entity is just a 64-bit ID; all data lives externally in component tables. The opposite of ORM thinking. This separation enables independent versioning, storage modes, and indexing per component.

    Four Things Learned from Databases

    1. ACID transactions + per-component MVCC. Traditional databases version whole rows. Typhon versions each component independently. An entity's PositionComponent and InventoryComponent each maintain their own revision chain—a 12-byte ring buffer with 48-bit transaction sequence numbers. Updating position doesn't create a new inventory version.

    2. Index-selective access. ECS iterates all matching entities every frame, but game servers often only need 1-4% of entities:

    | Scenario | Total Entities | Per Frame | Useful Work | |----------|---------------|-----------|-------------| | Battle royale (per-client relevance) | 50,000 | 500-2,000 | 1-4% | | MMO areas of interest | 100,000 | 200-1,000 | 0.2-1% | | Physics (active rigid bodies only) | All bodies | Active subset | 5-20% |

    Scanning everything means 25-100x unnecessary work. Databases solve this with B+Tree indexes. Typhon makes indexes first-class citizens in component storage.

    3. Spatial partitioning. Two spatial index layers integrated directly into component storage:

  • Layer 1: sparse hash table—O(1) rejection of empty regions
  • Layer 2: page-backed R-Tree—AABB, radius, ray, frustum, and kNN queries
  • Both layers run inside the same transaction model; no external data structures break cache locality.

    4. Persistence. WAL crash recovery, checkpoints, configurable fsync—things game servers need but ECS frameworks never provided.

    Three Storage Modes: Not All Data Is Equal

    | Mode | MVCC History | Persistence | Change Tracking | Use Case | |------|--------------|-------------|-----------------|----------| | Versioned | Full revision chain | WAL + checkpoints | MVCC | Inventory, economy, progression | | SingleVersion | Current state only | WAL + checkpoints | DirtyBitmap | Position, health, high-frequency updates | | Transient | Current state only | None | DirtyBitmap | AI blackboards, threat scores, pathfinding drafts |

    Same engine, same transaction API—but the storage layer does what each component type needs.

    Views: Bridging ECS Systems and Database Materialized Views

    view.Refresh() receives change pushes through a lock-free ring buffer—only entities whose indexed fields actually changed are re-evaluated. 100,000 entities match a view but only 12 changed? Do 12 evaluations, not 100,000.

    Part 3: Five Performance Principles

    Principle 1: Control Memory Layout

    Performance starts with struct definitions, not algorithms.

    Most dramatic example: switching from per-entity hash table lookups to cluster-based SoA storage—a 55x improvement, purely from memory layout:

    | Path | ns/entity | vs baseline | |------|-----------|-------------| | Standard EntityAccessor | 139 ns | 1.0x | | ArchetypeAccessor (cached) | 94 ns | 1.5x | | Cluster iteration | 2.5 ns | 55x |

    Cluster size isn't a magic constant. An auto-tuning algorithm evaluates N=8 to N=64, choosing the value that fits the most entities per 8KB page. Non-power-of-two often packs better: N=14 fits 28 entities per page while N=16 fits only 16.

    B+Tree node size is 256 bytes—because the CPU's Adjacent Line Prefetcher (ALP) automatically fetches paired 64-byte lines within 128-byte aligned regions. Two ALP triggers cover the whole node. A 256-byte node costs the same in memory access as a 128-byte node, but with nearly double the capacity.

    Principle 2: Eliminate Hot-Path Allocations

    Four-layer strategy:

  • ref struct: scoped access, invisible to GC
  • stackalloc: small temporary arrays (<64 elements)
  • Pinned Object Heap: large long-lived buffers GC won't compact
  • ArrayPool<T>: medium reusable buffers
  • Result: zero hot-path allocations at steady state.

    Principle 3: Reduce Memory Indirection

    Zone maps are the killer optimization—per-cluster min/max bounds maintained for each indexed field. A range query like WHERE Level >= 50 checks just two integers per cluster:

    | Selectivity | Without zone maps | With zone maps | Speedup | |-------------|-------------------|----------------|---------| | 100% | 13.4 ms | 1.3 ms | 10x | | 50% | 13.4 ms | 0.65 ms | 21x | | 10% | 13.4 ms | 0.16 ms | 84x | | 1% | 13.4 ms | 0.05 ms | 268x |

    Division elimination: integer division (idiv) costs 20-80 cycles; a magic multiplier replaces it in 3-4 cycles. Six lines of math, 20x speedup.

    Principle 4: Let the JIT Help

  • Constrained generics = monomorphization (same optimizations as Rust generics)
  • sealed = devirtualization (JIT converts virtual calls to direct calls and inlines)
  • static readonly = JIT dead code elimination (when disabled, entire if blocks vanish from native code—zero-cost observability)
  • SoA layout = JIT auto-vectorization (AVX2 processes 8 floats per instruction)

Principle 5: Design for the Hardware

Concurrency cost hierarchy:

| Level | Cost | Example | |-------|------|---------| | 0: Thread-local | ~2 ns | TLS counters | | 1: Uncontended atomics | 5-10 ns | Read latches | | 2: Contended atomics | 20-140 ns | Multiple writers, same lock | | 3: Syscalls | 500-1000 ns | Timestamps | | 4: Context switch | ~10,000 ns | Blocking locks | | 5: Oversubscription | 100,000+ ns | Threads > cores |

Every design decision maps to: stay as low as possible in this hierarchy.

Actual Performance Numbers

| Operation | Latency | |-----------|---------| | Cluster iteration (per entity) | 2.5 ns | | CRUD lifecycle (spawn/read/update/destroy/commit) | 2.95 µs | | Transaction create-read-commit (100 entities) | 3.6 µs | | B+Tree point lookup (10K entries) | 191 ns | | Component read (1 MVCC version) | 703 ns | | Component read (50 MVCC versions) | 720 ns | | Uncontended RW lock acquisition | 7.5 ns | | Cascading delete of 10K entities | 7.6 µs |

MVCC version count doesn't affect read performance: 50 versions vs 1 version read latency is nearly identical (720 ns vs 703 ns).

Parallel scaling: 8 workers achieve 7.1x speedup (89% efficiency). 16 workers drop to 6.7x (42%), hitting the 7950X's L3 cache / CCD boundary—a hardware wall, not a software wall.

Takeaways

1. Domain-specific safety beats general-purpose safety. Rust's borrow checker offers comprehensive guarantees at the cost of steep learning curves, long compile times, and inexpressible patterns. Typhon's approach: compile-time checks only on the unsafe operations you care about, with domain-semantic error messages—a more pragmatic engineering philosophy.

2. Two fields converging on the same solution is no coincidence. ECS and relational databases evolved independently for decades yet arrived at nearly identical storage structures (columnar storage, batch iteration, typed fields). The optimal data organization is dictated by hardware physics—cache line sizes, memory bandwidth, SIMD width—not industry convention.

3. "Not all data is equal" is an overlooked design principle. Most databases treat all data the same. Typhon's three storage modes acknowledge reality: in game servers, different data types have entirely different persistence needs and access patterns. Treating all three with one mechanism is either over-engineering (MVCC for positions) or under-protecting (no MVCC for inventory).

4. Implications for AI infrastructure. Though Typhon targets game servers, its ideas apply: choosing storage strategy by access pattern (KV caches, model weights, and activations all differ); zone-map-style early rejection for RAG retrieval; zero-copy blittable structs to avoid serialization in tensor passing.

5. What one person can do. Most striking of all: this is a solo developer project. 30 years of systems programming + deep hardware understanding + cross-domain insight into two fields = a database engine hitting microsecond latency in alpha. Deep cross-domain experience is a scarce superpower.

Coming Next

Part four (unreleased): "Deadlock-Free by Construction"—a mathematical proof via three pillars that deadlocks are structurally impossible. Not detection, not timeout retries—elimination by design.

Tags

#typhon#database-engine#csharp#dotnet#game-engines#ecs#performance#mvcc

This page is an English static mirror generated for search and AI citation. It may be a full translation or structured summary of the Chinese original. Canonical interactive discussion lives on the Chinese page: https://zhichai.net/topic/177618739