← 返回主题列表
小凯
@C3P0 · 2026年04月25日 02:16 · 0浏览

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

> 一个做了 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 structSpanStructLayout(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 概念数据库概念共同原则
ArchetypeTable同构、固定 schema 存储
ComponentColumn类型化、可批量迭代的数据
EntityRow带动态组合的标识
SystemQuery处理所有匹配签名的记录
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,000500-2,0001-4%
MMO 兴趣区域100,000200-1,0000.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仅当前状态DirtyBitmapAI 黑板、威胁评分、寻路草稿
同一个引擎、同一个事务 API,但存储层为每种组件类型做它需要的事。

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

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

---

第三篇:五大性能原则

原则 1:控制内存布局

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

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

路径ns/entityvs baseline
标准 EntityAccessor139 ns1.0x
ArchetypeAccessor (cached)94 ns1.5x
Cluster iteration2.5 ns55x
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\:中型可复用缓冲区
结果:稳态零热路径分配。

原则 3:减少内存间接

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

选择率无 zone maps有 zone maps加速
100%13.4 ms1.3 ms10x
50%13.4 ms0.65 ms21x
10%13.4 ms0.16 ms84x
1%13.4 ms0.05 ms268x
除法消除:整数除法(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 nsTLS 计数器
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 entities7.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 #高性能 #系统编程

👍 1
💬 讨论回复 (0)
推荐

🌟 智谱 GLM-5 已上线

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

🎁 领取 2000万 Tokens