- 数据库
- 缓存
- KV存储
【免费下载链接】FASTER
Fast persistent recoverable log and key-value store + cache, in C# and C++.
导读
本文以 FASTER C# 仓库中 cs/performance/BenchmarkDotNet/README.md 为核心,系统介绍 FASTER 如何借助 BenchmarkDotNet 建立微基准测试工程:覆盖 epoch 机制(LightEpoch)的 Acquire/Release 开销微基准、同步 API 与异步 API(Upsert/RMW/Read)的对比基准,以及基于 InliningDiagnoser 的内联情况诊断。读完本文,你将掌握该基准工程的构建与运行命令、三个现有测试的源码结构与实现细节,并能仿照现有测试为 FASTER 新增自己的 BenchmarkDotNet 基准用例。
一、背景:为什么 FASTER 需要 BenchmarkDotNet 微基准测试
FASTER 是一个高并发、持久化可恢复的日志与键值存储系统(提供 C# 与 C++ 实现)。对于这类以“低延迟、高吞吐”为核心诉求的存储组件,性能回归测试至关重要——任何一次对LightEpoch、分配器或 I/O 路径的改动,都可能带来几个数量级的吞吐变化。
BenchmarkDotNet 是一个支持在函数上添加注解的微基准测试套件,使用方式与单元测试类似:通过 Attribute 标记被测方法、配置参数组合,框架自动完成预热(Warmup)、多次迭代、统计置信区间与内存分配测量。FASTER 在仓库中维护了一个独立的 BenchmarkDotNet 测试工程,位于 cs/performance/BenchmarkDotNet/ 目录,目前包含三个“小而精”的基准类:
| 基准类 | 被测主题 | 源文件 |
|---|---|---|
LightEpochTests | epoch 的 Acquire/Release(Resume/Suspend)开销微基准 | LightEpochTests.cs |
SyncVsAsync | 同步 API 与异步 API(Upsert/RMW/Read)性能对比 | SyncVsAsyncTests.cs |
InliningTests | 输出哪些方法被 JIT 内联、哪些没有 | InliningTests.cs |
原 README 明确指出 “Currently we have two very simple benchmarks; more will be added later”(当前只有两个很简单的基准,未来会继续增加)——如今工程中已经扩展到了三个基准类,分别覆盖了 FASTER 性能的三个关键剖面:并发原语开销、API 形态差异、以及 JIT 内联情况。
二、工程结构与构建运行
2.1 工程文件与依赖
基准工程由 BenchmarkDotNetTests.csproj 定义:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net7.0</TargetFramework> </PropertyGroup> <PropertyGroup> <PlatformTarget>AnyCPU</PlatformTarget> <DebugType>pdbonly</DebugType> <DebugSymbols>true</DebugSymbols> <AllowUnsafeBlocks>true</AllowUnsafeBlocks> <Optimize>true</Optimize> <Configuration>Release</Configuration> <IsPackable>false</IsPackable> <ApplicationIcon /> <OutputType>Exe</OutputType> <StartupObject /> </PropertyGroup> <ItemGroup> <PackageReference Include="BenchmarkDotNet" Version="0.13.2" /> <PackageReference Include="BenchmarkDotNet.Diagnostics.Windows" Version="0.13.2" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\..\src\core\FASTER.core.csproj" /> </ItemGroup> </Project>关键点:
- TargetFramework 为
net7.0:以当前仓库的实际配置为准,运行基准前需确保本机已安装对应版本的 .NET SDK; - 依赖两个 BenchmarkDotNet 包:
BenchmarkDotNet0.13.2(核心框架)与BenchmarkDotNet.Diagnostics.Windows0.13.2(提供InliningDiagnoser等诊断器,见第三节); <Optimize>true</Optimize>且<Configuration>Release</Configuration>:微基准必须在 Release + 优化模式下运行,否则结果毫无参考价值;<AllowUnsafeBlocks>true</AllowUnsafeBlocks>:因为 FASTER.core 自身大量使用 unsafe 指针操作;- 通过
ProjectReference直接引用核心库 FASTER.core.csproj,保证基准始终基于当前源码构建,而非已安装的 NuGet 包。
2.2 程序入口:BenchmarkSwitcher
可执行程序的入口在 BenchmarkDotNetTestsApp.cs:
public static void Main(string[] args) { BenchmarkSwitcher.FromAssembly(typeof(BenchmarkDotNetTestsApp).Assembly).Run(args); }BenchmarkSwitcher.FromAssembly会自动扫描当前程序集内所有被[Benchmark]标记的类型,并根据命令行参数过滤出要运行的基准。因此新增基准类后无需修改入口代码——只要新类带有[Benchmark]方法,就会被 switcher 自动发现。
另外,该文件还定义了一个重要的公共属性(BenchmarkDotNetTestsApp.cs#L11):
public static string TestDirectory => Path.Combine(Path.GetDirectoryName(typeof(BenchmarkDotNetTestsApp).Assembly.Location), "Tests");它返回程序集所在目录下的Tests子目录。SyncVsAsyncTests和InliningTests中的日志设备文件都创建在这个目录下,运行完基准后由GlobalCleanup清理删除。
2.3 构建与运行命令
README 给出的运行步骤是:
构建 Release 版本(Release 配置,因为 BenchmarkDotNet 要求以 Release 模式运行以获得真实性能数据):
dotnet build -c Release cs/performance/BenchmarkDotNet/BenchmarkDotNetTests.csproj执行基准,可用
-f通配符过滤器只运行某一类测试。README 中给出的示例是运行 LightEpoch 相关测试:bin/Release/[platform]/BenchmarkDotNetTests.exe -f *LightEpoch*[platform]取决于操作系统与目标框架(如net7.0)。在 Windows 上直接运行 exe;在 Linux/macOS 上则可等价使用:dotnet run -c Release --project cs/performance/BenchmarkDotNet/BenchmarkDotNetTests.csproj -- -f *LightEpoch*-f(即--filter)支持*通配符,因此:-f *LightEpoch*:仅运行LightEpochTests;-f *SyncVsAsync*:仅运行SyncVsAsync;-f *Inlining*:仅运行InliningTests;- 不带
-f参数:运行程序集中的全部基准(耗时最长,不推荐在调试阶段使用)。
查看输出:BenchmarkDotNet 会打印每个基准在各参数组合下的 Mean、Error、StdDev、Gen0/Gen1/Gen2 分配等统计信息,并将完整结果写入
BenchmarkDotNet.Artifacts目录(含 Markdown 摘要、JSON 原始数据与图表)。
三、LightEpochTests:epoch Acquire/Release 开销微基准
3.1 基准源码
LightEpochTests.cs 是三个基准中最简单、也最贴近 FASTER 并发核心的一个:
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory, BenchmarkLogicalGroupRule.ByParams)] public class LightEpochTests { [Params(10_000_000, 100_000_000, 1_000_000_000)] public int NumIterations; [BenchmarkCategory("LightEpoch"), Benchmark] public void AcquireAndRelease() { var epoch = new LightEpoch(); for (int i = 0; i < NumIterations; i++) { epoch.Resume(); epoch.Suspend(); } } }要点解读:
[Params(10_000_000, 100_000_000, 1_000_000_000)](LightEpochTests.cs#L15):设置三档迭代次数(1000 万、1 亿、10 亿),BenchmarkDotNet 会对每个参数组合各跑一轮完整测量;[GroupBenchmarksBy(...)]:按 BenchmarkCategory 与 Params 分组,保证不同参数组合的结果在报表中清晰并列;- 被测逻辑就是反复执行
epoch.Resume(); epoch.Suspend();——即 README 中所说的 epoch 的 Acquire(获取)/Release(释放)微基准。每次循环新建一个LightEpoch实例其实会引入构造开销,但本基准的意图是测量最坏情况下的每轮 Acquire/Release 成本,并作为后续优化的基线。
3.2 源码纵深:LightEpoch 的 Acquire/Release 到底是什么
LightEpoch是 FASTER 并发控制的基石,实现位于 cs/src/core/Epochs/LightEpoch.cs。从基准代码中调用的Resume()和Suspend()可以看到它的两条核心路径:
Resume(Acquire)路径(LightEpoch.cs#L216-L220):
public void Resume() { Acquire(); ProtectAndDrain(); }Suspend(Release)路径(LightEpoch.cs#L206-L210):
public void Suspend() { Release(); if (drainCount > 0) SuspendDrain(); }从源码可以推断出它的设计意图:
Acquire()(LightEpoch.cs#L419-L429)负责为当前线程在共享的 epoch 表中保留一个槽位(首次调用通过ReserveEntryForThread分配,之后复用[ThreadStatic]的threadEntryIndex),并递增该线程的嵌套计数。注意其中带有一条Debug.Assert,提示“不要在 FASTER 回调或 IDevice 实现中重入受保护的 epoch;若使用 Task,请采用TaskCreationOptions.RunContinuationsAsynchronously”——这正是LightEpoch使用中的典型陷阱。ProtectAndDrain()(LightEpoch.cs#L188-L200)把当前全局CurrentEpoch写入本线程的localCurrentEpoch槽位,标记本线程“处于受保护区域”;随后若drainCount > 0,则尝试排空待执行的回收动作。Release()(LightEpoch.cs#L435-L453)则清空本线程的localCurrentEpoch与threadId,在嵌套计数归零后释放静态线程表槽位。SuspendDrain()(LightEpoch.cs#L364-L382)保证最后一个挂起(Suspend)的线程负责执行所有挂起的回收动作——这是“线程退出即排空”的协作式回收语义。
LightEpoch的核心数据结构是一个按 64 字节缓存行对齐的Entry表(LightEpoch.cs#L524-L546),每项localCurrentEpoch+threadId+ 6 个markers;表大小kTableSize = max(128, 2 × CPU 核数)(LightEpoch.cs#L66)。全局安全回收水位由ComputeNewSafeToReclaimEpoch(LightEpoch.cs#L339-L358)扫描所有线程槽位取“最老进行中调用”的前一代计算得出。这些机制共同保证了:任何内存回收动作都只能发生在所有线程都离开旧 epoch 之后,这正是无锁哈希索引中“延迟回收(deferred reclamation)”的根基。
此外,LightEpoch还通过BumpCurrentEpoch(Action onDrain)(LightEpoch.cs#L243-L290)将回收动作与特定 epoch 关联,并在其上构建了EpochProtectedVersionScheme(EPVS,见 cs/src/core/Epochs/EpochProtectedVersionScheme.cs),用于 checkpoint 状态机等版本化操作。整个LightEpoch类被FASTER.core中分配器(AllocatorBase.cs#L50)、扫描迭代器(ScanIteratorBase.cs#L33)、存储设备(StorageDeviceBase.cs#L63)等多处持有——因此它的 Acquire/Release 开销直接进入 FASTER 每一条读写热路径,对该路径做微基准的意义不言而喻。
四、SyncVsAsyncTests:同步与异步 API 性能对比
4.1 基准源码结构
SyncVsAsyncTests.cs 对比了 FASTER 会话 API 的同步与异步形态,覆盖三大操作类别:Insert(Upsert)、RMW(Read-Modify-Write)、Read。其参数为记录数:
[Params(100, 1_000_000)] public int NumRecords;(SyncVsAsyncTests.cs#L18)
存储准备(SyncVsAsyncTests.cs#L28-L38):
void SetupStore() { logDirectory = BenchmarkDotNetTestsApp.TestDirectory; var logFilename = Path.Combine(logDirectory, $"{nameof(SyncVsAsync)}_{Guid.NewGuid()}.log"); logDevice = Devices.CreateLogDevice(logFilename, preallocateFile: true, deleteOnClose: true, useIoCompletionPort: true); var logSettings = new LogSettings { LogDevice = logDevice }; store = new FasterKV<long, long>(1L << 20, logSettings); }几个值得注意的实操细节:
Devices.CreateLogDevice(其完整签名见 cs/src/core/Device/Devices.cs#L29)是 FASTER 的设备工厂方法,这里使用了preallocateFile: true(预分配日志文件)、deleteOnClose: true(关闭即删除,适合基准场景,避免残留文件)、useIoCompletionPort: true(Windows 上使用 IOCP 轮询完成 IO);new FasterKV<long, long>(1L << 20, logSettings)创建哈希表大小为 2^20 的 FasterKV 实例,键值类型均为long(blittable,走无托管 GC 的快路径);[GlobalSetup]与[GlobalCleanup]是 BenchmarkDotNet 的生命周期钩子:SetupEmptyStore只创建空存储,服务于 Insert 类基准;SetupPopulatedStore额外用同步 Upsert 预先填充NumRecords条记录,服务于 RMW/Read 类基准;TearDown负责 Dispose 存储与设备并删除日志目录。
同步填充与异步填充(SyncVsAsyncTests.cs#L41-L56):
void PopulateStoreSync() { using var session = store.For(new SimpleFunctions<long, long>()).NewSession<SimpleFunctions<long, long>>(); for (long ii = 0; ii < NumRecords; ++ii) session.Upsert(ii, ii); } async ValueTask PopulateStoreAsync() { using var session = store.For(new SimpleFunctions<long, long>()).NewSession<SimpleFunctions<long, long>>(); for (long ii = 0; ii < NumRecords; ++ii) { var result = await session.UpsertAsync(ii, ii); while (result.Status.IsPending) result = await result.CompleteAsync().ConfigureAwait(false); } }这里展示了 FASTER 异步 API 的标准 pending 处理模式:UpsertAsync/RMWAsync/ReadAsync返回的结果带有Status,当操作落到磁盘/待处理队列时会进入IsPending状态,必须循环调用CompleteAsync()直到完成。这一循环是 FASTER 异步会话编程的核心范式,在仓库的 Async 实现(如 cs/src/core/Async/UpsertAsync.cs)中可以看到其对应的状态推进逻辑。
三类对比基准(SyncVsAsyncTests.cs#L82-L133):
[BenchmarkCategory("Insert"), Benchmark(Baseline = true)] public void InsertSync() => PopulateStoreSync(); [BenchmarkCategory("Insert"), Benchmark] public ValueTask InsertAsync() => PopulateStoreAsync(); [BenchmarkCategory("RMW"), Benchmark(Baseline = true)] public void RMWSync() { ... session.RMW(ii, ii * 2); session.CompletePending(); } [BenchmarkCategory("RMW"), Benchmark] public async ValueTask RMWAsync() { ... } [BenchmarkCategory("Read"), Benchmark(Baseline = true)] public void ReadSync() { ... 若 IsPending 则 CompletePendingWithOutputs ... } [BenchmarkCategory("Read"), Benchmark] public async ValueTask ReadAsync() { ... }每个类别都以同步版本作为Baseline = true,异步版本与之对比——BenchmarkDotNet 报表会直接给出异步相对同步的比值(Ratio),回答“异步 API 是否有额外开销、开销多大”这一关键工程问题。同步路径中的session.CompletePending()、CompletePendingWithOutputs(out var completedOutputs, wait: true)是同步会话补齐 pending 操作的标准收尾方式。
值得指出的是,源码中被注释掉的[ParamsAllValues] bool useAsync;(SyncVsAsyncTests.cs#L21-L22)表明作者曾设想把同步/异步也做成参数组合维度,最终选择了分别写独立 Benchmark 方法的方式——这也是 BenchmarkDotNet 中对比两种 API 形态的推荐做法。被测函数SimpleFunctions<long, long>定义于 cs/src/core/Index/Interfaces/FunctionsBase.cs#L121-L124,是内置的最小函数集实现。
五、InliningTests:JIT 内联诊断
5.1 基准源码
InliningTests.cs 的目标不是测吞吐,而是输出 FASTER.core 中哪些方法被 JIT 内联、哪些没有——内联决策直接影响热路径的调用开销与寄存器压力,是性能工程师排查“为什么优化没生效”的重要工具。
[InliningDiagnoser(logFailuresOnly: false, allowedNamespaces: new[] { "FASTER.core" })] [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory, BenchmarkLogicalGroupRule.ByParams)] public class InliningTests { [Params(1_000_000)] public int NumRecords; ... }(InliningTests.cs#L15-L19)
关键点:
[InliningDiagnoser(...)]来自BenchmarkDotNet.Diagnostics.Windows包(这也解释了 csproj 中为何要引入该包)。logFailuresOnly: false表示同时记录成功与失败的内联;allowedNamespaces: new[] { "FASTER.core" }将诊断范围限定在 FASTER.core 命名空间,避免被无关框架代码刷屏;- 它只跑 100 万条记录的三个操作:Upsert、RMW、Read(InliningTests.cs#L66-L91),与
SyncVsAsync共享同一套SetupStore/PopulateStore模式,说明这两个基准类针对同一批操作从不同视角分析。
运行后,InliningDiagnoser 会为每个被测方法生成 HTML 报告,标注每个调用点的内联结果(成功/失败及原因,如“too big”“has locals”等)。FASTER 源码中大量使用了[MethodImpl(MethodImplOptions.AggressiveInlining)](例如LightEpoch的Resume/Suspend/ProtectAndDrain,见 LightEpoch.cs#L187-L220),这份报告能直观验证这些注解是否按预期生效。
5.2 三个基准的协同意义
把三个基准放在一起看,它们构成了 FASTER 性能工程的完整闭环:
- LightEpochTests回答“并发原语本身多快”;
- SyncVsAsyncTests回答“API 形态选择带来多大差异”;
- InliningTests回答“关键路径代码是否被编译器友好对待”。
任何一个维度出现问题,都能在对应基准中暴露出来。
六、跨运行比较的限制与应对
README 明确记录了一个 BenchmarkDotNet 的已知限制:
BenchmarkDotNet does not support cross-run comparisons; currently, just save the output to a file and diff. A script will be written in the future to automate these comparisons.
即BenchmarkDotNet 不支持跨多次运行的直接对比。这是因为每次运行的环境(机器负载、JIT 决策、ASLR 等)都不同,官方工具只保证单次运行内部各基准之间的可比性。当前仓库的做法是:
- 每次运行后把 BenchmarkDotNet 输出的 Markdown/JSON 结果保存为文件(BenchmarkDotNet 默认输出到
BenchmarkDotNet.Artifacts); - 通过 diff 工具对比改动前后的结果文件;
- 未来计划编写脚本自动化这一对比流程(README 原话,可作为仓库 roadmap 的参考)。
这也解释了为何 README 中给的是-f *LightEpoch*这类带过滤器的运行方式——每次聚焦单一基准、保存结果、再做跨版本 diff,是当前约束下的推荐工作流。
七、如何新增一个基准测试
README 给出了新增基准的指引:“以现有测试为范例,并在本文档中简要登记新增条目”。结合本工程的既有模式,新增一个基准的推荐步骤是:
- 在 cs/performance/BenchmarkDotNet/ 目录下新建
YourFeatureTests.cs,类上加[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory, BenchmarkLogicalGroupRule.ByParams)]; - 用
[Params(...)]声明关键变量(如记录数、线程数),用[BenchmarkCategory("...")] + [Benchmark]声明被测方法;若想作为对照组,加[Benchmark(Baseline = true)]; - 若涉及 FasterKV 实例,复用
SetupStore/PopulateStore/TearDown模式(GlobalSetup/GlobalCleanup),日志设备文件统一放在BenchmarkDotNetTestsApp.TestDirectory下并deleteOnClose: true; - 无需修改入口:
BenchmarkSwitcher.FromAssembly会自动发现新基准类(BenchmarkDotNetTestsApp.cs#L15); - 运行验证:
dotnet build -c Release后执行-f *YourFeature*,确认能正常出结果; - 按 README 约定,在 README.md 的测试清单中补一行简要说明。
八、小结
FASTER 的 BenchmarkDotNet 基准工程虽然规模不大,却精准覆盖了存储引擎性能的三个决定性剖面:epoch 并发原语(LightEpoch)的单次 Acquire/Release 开销、同步与异步会话 API 的开销差、以及 FASTER.core 热路径方法的 JIT 内联情况。工程结构(switcher 入口 + 按主题拆分的基准类 + 共享的日志设备管理模式)让新增基准的成本极低。对 FASTER 的贡献者而言,这套工程是评估核心库改动性能影响的标准工具;对研究 FASTER 实现原理的读者而言,从 LightEpochTests.cs 出发回溯 LightEpoch.cs 的 Acquire/Release 与 drain 机制,是理解其无锁并发回收设计的绝佳入口。
参考路径索引
- 基准工程 README:cs/performance/BenchmarkDotNet/README.md
- 入口与工程配置:BenchmarkDotNetTestsApp.cs、BenchmarkDotNetTests.csproj
- 三个基准类:LightEpochTests.cs、SyncVsAsyncTests.cs、InliningTests.cs
- 被测核心实现:LightEpoch.cs、EpochProtectedVersionScheme.cs、Devices.cs、FunctionsBase.cs
- 数据库
- 缓存
- KV存储
【免费下载链接】FASTER
Fast persistent recoverable log and key-value store + cache, in C# and C++.
相关推荐
如何用 Rufus 免费制作 Windows 11 启动 U 盘:绕过 TPM 限制的完整指南
如何用 Rufus 免费制作 Windows 11 启动 U 盘:绕过 TPM 限制的完整指南 升级 Windows 11 时,安装程序提示"此电脑不满足最低系
桌面应用开发工具BenchmarkDotNet 基准测试 IAsyncEnumerable<T>:迭代驱动、取消传播与 Disassembly 诊断完全指南
BenchmarkDotNet 基准测试 IAsyncEnumerable<T :迭代驱动、取消传播与 Disassembly 诊断完全指南 Benchmark
性能测试开发工具3分钟掌握微信防撤回补丁:永久保存重要信息不丢失
3分钟掌握微信防撤回补丁:永久保存重要信息不丢失 在日常使用微信、QQ和TIM等即时通讯工具时,你是否经常遇到重要消息被对方撤回而无法查看的困扰?无论是工作中的
桌面应用即时通讯
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考