news 2026/11/5 7:36:05

FASTER 基准测试指南:基于 BenchmarkDotNet 的 LightEpoch、同步/异步 API 与内联诊断测试深入解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
FASTER 基准测试指南:基于 BenchmarkDotNet 的 LightEpoch、同步/异步 API 与内联诊断测试深入解析
  • 数据库
  • 缓存
  • KV存储

【免费下载链接】FASTER

Fast persistent recoverable log and key-value store + cache, in C# and C++.

项目地址:https://gitcode.com/gh_mirrors/fa/FASTER
点击查看免费下载

导读

本文以 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/ 目录,目前包含三个“小而精”的基准类:

基准类被测主题源文件
LightEpochTestsepoch 的 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 给出的运行步骤是:

  1. 构建 Release 版本(Release 配置,因为 BenchmarkDotNet 要求以 Release 模式运行以获得真实性能数据):

    dotnet build -c Release cs/performance/BenchmarkDotNet/BenchmarkDotNetTests.csproj
  2. 执行基准,可用-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参数:运行程序集中的全部基准(耗时最长,不推荐在调试阶段使用)。
  3. 查看输出: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 等)都不同,官方工具只保证单次运行内部各基准之间的可比性。当前仓库的做法是:

  1. 每次运行后把 BenchmarkDotNet 输出的 Markdown/JSON 结果保存为文件(BenchmarkDotNet 默认输出到BenchmarkDotNet.Artifacts);
  2. 通过 diff 工具对比改动前后的结果文件;
  3. 未来计划编写脚本自动化这一对比流程(README 原话,可作为仓库 roadmap 的参考)。

这也解释了为何 README 中给的是-f *LightEpoch*这类带过滤器的运行方式——每次聚焦单一基准、保存结果、再做跨版本 diff,是当前约束下的推荐工作流。

七、如何新增一个基准测试

README 给出了新增基准的指引:“以现有测试为范例,并在本文档中简要登记新增条目”。结合本工程的既有模式,新增一个基准的推荐步骤是:

  1. 在 cs/performance/BenchmarkDotNet/ 目录下新建YourFeatureTests.cs,类上加[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory, BenchmarkLogicalGroupRule.ByParams)];
  2. 用[Params(...)]声明关键变量(如记录数、线程数),用[BenchmarkCategory("...")] + [Benchmark]声明被测方法;若想作为对照组,加[Benchmark(Baseline = true)];
  3. 若涉及 FasterKV 实例,复用SetupStore/PopulateStore/TearDown模式(GlobalSetup/GlobalCleanup),日志设备文件统一放在BenchmarkDotNetTestsApp.TestDirectory下并deleteOnClose: true;
  4. 无需修改入口:BenchmarkSwitcher.FromAssembly会自动发现新基准类(BenchmarkDotNetTestsApp.cs#L15);
  5. 运行验证:dotnet build -c Release后执行-f *YourFeature*,确认能正常出结果;
  6. 按 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++.

项目地址:https://gitcode.com/gh_mirrors/fa/FASTER
点击查看免费下载

相关推荐

上一篇:wiliwili:解锁游戏主机的B站娱乐新体验
下一篇:如何用Zotero-mdnotes实现高效文献管理与知识自动化

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/25 13:04:26

Sunshine 入门实战:10分钟把PC游戏投到大屏

Sunshine 入门实战&#xff1a;10分钟把PC游戏投到大屏 【免费下载链接】Sunshine Self-hosted game stream host for Moonlight. 项目地址: https://gitcode.com/GitHub_Trending/su/Sunshine 电视大屏摆在那&#xff0c;游戏却全装在 PC 上。Sunshine 就是解决这件事的…

作者头像 李华
网站建设 2026/10/22 20:40:03

IT监控自动化落地指南:从重复告警到自愈闭环

监控做到一定阶段&#xff0c;大家会发现一个很尴尬的事&#xff1a;监控系统本身变成了最大的日常维护负担。机器一多、告警一多&#xff0c;运维团队每天不是在处理告警&#xff0c;就是在处理告警的路上。于是很多人开始提“IT 监控自动化”&#xff0c;想靠自动化把监控这个…

作者头像 李华