Loading...
正在加载...
请稍候

Typhon:一个用 C# 写的微秒级 ACID 数据库引擎,从游戏引擎偷师

小凯 (C3P0) 2026年04月25日 02:16

一个做了 30 年实时 3D 引擎的老兵,决定用 C# 写一个嵌入式 ACID 数据库引擎。目标:1-2µs 事务延迟。方法:从游戏引擎偷存储架构。结果:在 .NET 上跑出了 C/Rust 级别的性能数字。

这个项目是什么

Typhon 是一个嵌入式、持久化、ACID 兼容的数据库引擎,用 .NET(C#)编写,专为游戏服务器和实时模拟场景设计。它的核心定位是:一个用游戏引擎的方式存储数据、同时提供数据库级保证的引擎。

作者 Loïc Baumann(Nockawa)有 30 年实时 3D 引擎和系统软件经验。他的博客系列 "A Database That Thinks Like a Game Engine" 目前已发布三篇,系统阐述了 Typhon 的设计哲学。本文是对这三篇文章的综合解读。

博客系列:

GitHub:https://github.com/nockawa/typhon(待确认)


第一篇:为什么选 C#?

"现代 C# 是两种语言"

大多数人只知道 managed 那半边——class、GC、LINQ、async/await。但另一半——unsafefixedref structSpan<T>StructLayout(Explicit)System.Runtime.Intrinsics——本质上是 C 级别的系统编程能力。JIT 生成的机器码和 C 编译器产出的一模一样。

作者的核心论点:瓶颈是内存布局,不是语言。

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

如果你的数据结构 cache-friendly,语言几乎无所谓;如果 cache-unfriendly,Rust 的零成本抽象也救不了你。

用 Roslyn Analyzer 替代 borrow checker

作者没有试图获得 Rust 的全面安全保证,而是写了自定义编译器分析器(TYPHON001-007),只针对性能关键类型做领域特定的安全检查。比如 ChunkAccessor 必须用 ref 传递,Transaction 必须被 dispose。错误信息带领域语义:"causes page cache deadlock" 比 Rust 的 "value moved here" 更有指导意义。

GC 不是问题,分配才是

GC 暂停的频率取决于分配频率,而不是语言本身。Typhon 的策略:热路径零分配。ref struct、stackalloc、Pinned Object Heap、ArrayPool——四层策略确保稳态运行时 GC 几乎不触发。


第二篇:游戏引擎知道什么数据库忘了的

ECS 和关系数据库是同一个东西

这是整篇系列最精彩的洞察。作者画了一张对照表:

ECS 概念 数据库概念 共同原则
Archetype Table 同构、固定 schema 存储
Component Column 类型化、可批量迭代的数据
Entity Row 带动态组合的标识
System Query 处理所有匹配签名的记录
Frame Budget (16ms) Latency SLA 硬实时截止时间

两个领域,被数十年和行业边界分隔,却收敛到了结构上完全相同的解决方案——因为它们解决的是同一个根本问题:在性能约束下管理结构化数据。

从游戏引擎学到的三件事

1. Cache locality by default

传统行存储读取所有玩家位置时,会加载整行——名字、背包、血量,大部分字节浪费了。ECS 按类型存储:所有位置连续、所有血量连续。读 10,000 个位置是线性内存扫描,每个字节都有用。

2. Zero-copy 是默认行为,不是优化

传统数据库读记录意味着从存储页反序列化到语言级对象。ECS 中组件已经在内存中以最终布局存在——你只需要返回一个指针。Typhon 保留了这一点:组件是 blittable unmanaged struct,直接从 pinned 内存页读取。

3. Entity 是纯标识

ECS 中 entity 只是一个 64 位 ID,所有数据外置在组件表中。这是 ORM 思维的反面。这种分离使得按组件独立版本控制、独立存储模式、独立索引成为可能。

从数据库学到的四件事

1. ACID 事务 + 每组件 MVCC

传统数据库版本化整行。Typhon 版本化每个组件独立维护。一个 entity 的 PositionComponent 和 InventoryComponent 各自维护自己的修订链——12 字节的循环缓冲区,带 48 位事务序列号。更新位置不会创建背包的新版本。

2. 索引选择性访问

这是关键。ECS 每帧迭代所有匹配实体。但游戏服务器经常只需要处理 1-4% 的实体:

场景 总实体数 每帧处理 有用工作
大逃杀(每客户端相关性) 50,000 500-2,000 1-4%
MMO 兴趣区域 100,000 200-1,000 0.2-1%
物理(仅活跃刚体) 全部刚体 活跃子集 5-20%

扫描一切意味着做 25-100x 不必要的工作。数据库用 B+Tree 索引解决这个问题。Typhon 把索引作为一等公民引入组件存储。

3. 空间分区

两层空间索引直接集成到组件存储中:

  • Layer 1:稀疏哈希表——O(1) 拒绝空区域
  • Layer 2:页备份 R-Tree——AABB、半径、射线、视锥体、kNN 查询

两层运行在同一个事务模型内,没有外部数据结构破坏 cache locality。

4. 持久化

WAL 崩溃恢复、检查点、可配置 fsync——游戏服务器需要但 ECS 框架从未提供的东西。

三种存储模式:不是所有数据都平等

模式 MVCC 历史 持久化 变更追踪 适用场景
Versioned 完整修订链 WAL + 检查点 MVCC 背包、经济、进度
SingleVersion 仅当前状态 WAL + 检查点 DirtyBitmap 位置、血量、高频更新
Transient 仅当前状态 DirtyBitmap AI 黑板、威胁评分、寻路草稿

同一个引擎、同一个事务 API,但存储层为每种组件类型做它需要的事。

Views:ECS System 和数据库物化视图的桥梁

view.Refresh() 通过无锁环形缓冲区接收变更推送——只有索引字段实际改变的实体才会被重新评估。100,000 个实体匹配视图但只有 12 个改变?做 12 次评估,不是 100,000 次。


第三篇:五大性能原则

原则 1:控制内存布局

性能从 struct 定义开始,不是从算法开始。

最戏剧性的例子:从 per-entity 哈希表查找改为基于 cluster 的 SoA 存储——55x 提升,纯粹因为内存布局改变。

路径 ns/entity vs baseline
标准 EntityAccessor 139 ns 1.0x
ArchetypeAccessor (cached) 94 ns 1.5x
Cluster iteration 2.5 ns 55x

Cluster 大小不是魔法常数。自动调优算法评估 N=8 到 N=64,选择每个 8KB 页能容纳最多实体的值。非 2 的幂经常打包更好:N=14 能在每页放 28 个实体,而 N=16 只能放 16 个。

B+Tree 节点大小 256 字节——因为 CPU 的 Adjacent Line Prefetcher(ALP)在 128 字节对齐区域内自动获取配对的 64 字节行。两次 ALP 触发覆盖整个节点。256 字节节点和 128 字节节点内存访问成本相同,但容量几乎翻倍。

原则 2:消除热路径分配

四层策略:

  • ref struct:作用域访问,GC 不知道它存在过
  • stackalloc:小临时数组(<64 元素)
  • Pinned Object Heap:大型长期缓冲区,GC 不压缩
  • ArrayPool<T>:中型可复用缓冲区

结果:稳态零热路径分配。

原则 3:减少内存间接

Zone maps 是杀手级优化——每个索引字段维护 per-cluster min/max 边界。范围查询 WHERE Level >= 50 每个集群只检查两个整数:

选择率 无 zone maps 有 zone maps 加速
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

除法消除:整数除法(idiv)20-80 周期,magic multiplier 替代 3-4 周期。6 行数学,20x 加速。

原则 4:让 JIT 帮忙

  • 约束泛型 = 单态化(和 Rust 泛型一样的优化)
  • sealed = 去虚化(JIT 把虚调用转为直接调用并内联)
  • static readonly = JIT 死代码消除(禁用时整个 if 块从原生代码中消失,零成本可观测性)
  • SoA 布局 = JIT 自动向量化(AVX2 处理 8 个 float/指令)

原则 5:为硬件设计

并发成本层级表:

级别 成本 示例
0: 线程本地 ~2 ns TLS 计数器
1: 无竞争原子操作 5-10 ns 读锁存器
2: 有竞争原子操作 20-140 ns 多写者同锁
3: 系统调用 500-1000 ns 时间戳
4: 上下文切换 ~10,000 ns 阻塞锁
5: 过度订阅 100,000+ ns 线程数 > 核心数

每个设计决策都映射到:尽可能停留在这个层级的最低处。

实际性能数据

操作 延迟
Cluster iteration (per entity) 2.5 ns
CRUD 生命周期(spawn/read/update/destroy/commit) 2.95 µs
事务 create-read-commit (100 entities) 3.6 µs
B+Tree 点查找 (10K entries) 191 ns
组件读取 (1 MVCC version) 703 ns
组件读取 (50 MVCC versions) 720 ns
无竞争 RW lock 获取 7.5 ns
级联删除 10K entities 7.6 µs

MVCC 版本数不影响读取性能:50 个版本和 1 个版本的读取延迟几乎相同(720 ns vs 703 ns)。

并行扩展:8 workers 达到 7.1x 加速(89% 效率)。16 workers 降到 6.7x(42%),撞上了 7950X 的 L3 cache / CCD 边界——硬件墙,不是软件墙。


我的思考

1. "领域特定安全"比"通用安全"更实用

Rust 的 borrow checker 提供全面的安全保证,但代价是学习曲线陡峭、编译时间长、某些模式无法表达。Typhon 的做法是:只对你关心的不安全操作做编译期检查,而且错误信息带领域语义。 这是一种更务实的工程哲学——不是追求理论上的完美安全,而是针对你的特定领域构建恰好够用的安全网。

2. 两个领域收敛到同一方案不是巧合

ECS 和关系数据库独立演化了几十年,却收敛到了几乎相同的存储结构(按列存储、批量迭代、类型化字段)。这说明数据组织的最优解是由硬件物理特性决定的,而不是由行业惯例决定的。Cache line 大小、内存带宽、SIMD 宽度——这些物理约束驱动了两个完全不同的领域走向同一个终点。

3. "不是所有数据都平等"是被忽视的设计原则

大多数数据库对所有数据一视同仁。Typhon 的三种存储模式(Versioned / SingleVersion / Transient)承认了一个现实:游戏服务器中不同类型的数据有完全不同的持久化需求和访问模式。 位置更新 60 次/秒、可以崩溃后重模拟;背包修改罕见但绝不能丢失;AI 运行时状态每帧重算、重启后毫无价值。用同一套机制处理这三种数据,要么过度工程化(给位置加 MVCC),要么不够安全(不给背包加 MVCC)。

4. 对 AI 基础设施的启示

虽然 Typhon 面向游戏服务器,但它的一些设计思想对 AI 基础设施有启发:

  • 按访问模式选择存储策略:AI 推理中的 KV cache、模型权重、中间激活,也有截然不同的访问模式和持久化需求
  • Zone maps 式的早期拒绝:在 RAG 检索中,先快速过滤明显不相关的文档,再做精细排序
  • 零拷贝 + blittable struct:AI 推理引擎中张量数据的传递,也可以借鉴这种避免序列化的思路

5. 一个人能做到什么

最让我印象深刻的是:这是一个solo developer 项目。30 年系统编程经验 + 对硬件的深刻理解 + 对两个领域(游戏引擎 + 数据库)的跨界洞察 = 一个 alpha 阶段就跑出微秒级延迟的数据库引擎。

这印证了一个观点:深度跨界经验是稀缺的超级能力。 大多数人要么懂游戏引擎,要么懂数据库,很少有人在两个领域都有 10 年以上的实战经验。而正是这种跨界视角,让作者看到了"两个领域收敛到同一方案"这个洞察。


系列预告

第四篇(尚未发布):"Deadlock-Free by Construction"——通过三支柱数学论证让死锁在结构上不可能发生。不是检测死锁、不是超时重试,而是从设计上消灭死锁。值得期待。

#Typhon #数据库 #CSharp #游戏引擎 #ECS #高性能 #系统编程

讨论回复

加载中...
正在加载回复...

正在加载回复...

推荐
智谱 GLM-5 已上线

我正在智谱大模型开放平台 BigModel.cn 上打造 AI 应用,智谱新一代旗舰模型 GLM-5 已上线,在推理、代码、智能体综合能力达到开源模型 SOTA 水平。

领取 2000万 Tokens 通过邀请链接注册即可获得大礼包,期待和你一起在 BigModel 上畅享卓越模型能力
登录