- 性能测试
- 开发工具
【免费下载链接】BenchmarkDotNet
Powerful .NET library for benchmarking
在 BenchmarkDotNet 中,默认的基准测试会为每个基准生成一个独立的控制台程序并在单独的进程中运行,以实现进程级隔离。而 InProcess(进程内)工具链则完全绕开这一流程——它不生成任何新的可执行文件,而是在宿主进程内部通过动态发射 IL 的方式直接运行基准,适合追求极速启动、或需要在不被 BenchmarkDotNet 支持的运行时(例如本地编译的 CoreCLR)上运行基准的场景。读完本文,你将掌握[InProcess]特性的用法、InProcessEmitToolchain与InProcessNoEmitToolchain的差异、配套的InProcessValidator校验规则,以及如何在一个 Benchmark 类中同时对比进程内与进程外两种运行方式的实测差异。
什么是 InProcess 工具链
按照 toolchains.md 的定义,为了获得进程级隔离,BenchmarkDotNet 默认会为每一个基准"生成、构建并执行一个新的控制台应用"。一个toolchain(工具链)由 generator(生成器)、builder(构建器)和 executor(执行器)三部分组成:
- 未显式指定工具链时,Full .NET Framework 与 Mono 默认使用 Roslyn 工具链,.NET Core 与 NativeAOT 默认使用 dotnet cli 工具链;
- 而InProcess 工具链则完全不同:它不生成新的可执行文件,直接在调用方进程内完成基准运行。
在 IntroInProcess.md 中,官方对InProcessEmitToolchain给出了这样的定位:
InProcessEmitToolchain is our toolchain which does not generate any new executable. It emits IL on the fly and runs it from within the process itself.
即:在运行时动态发射 IL(Emit IL on the fly),并在当前进程内部执行。它的典型使用场景有两个:
- 追求极快的基准启动与运行速度——省去了每次运行都生成项目、调用编译器构建、再拉起子进程的开销;
- 在 BenchmarkDotNet 不支持的运行时上运行基准——例如你自己本地构建(local build)的 CoreCLR,因为基准直接跑在宿主进程的运行时之上,无需为它单独适配工具链。
快速上手:[InProcess]特性
使用 InProcess 工具链最简洁的方式是在基准类上直接标注[InProcess]特性。官方文档给出的最小示例是:
[InProcess] public class TypeWithBenchmarks { }从源码看,这个特性定义在 src/BenchmarkDotNet/Attributes/Jobs/InProcessAttribute.cs,它是一个JobConfigBaseAttribute,支持两个可选参数:
public InProcessAttribute( InProcessToolchainType toolchainType = InProcessToolchainType.Auto, bool executeOnSeparateThread = true)toolchainType:取值为Auto、Emit、NoEmit三选一(对应枚举InProcessToolchainType)。当为Auto时,特性会检测当前进程是否运行在 AOT 环境(RuntimeInformation.IsAot):若是则自动选择NoEmit,否则选择Emit。原因在于 AOT 场景下没有动态 IL 发射能力,只能退而使用基于反射的 NoEmit 实现;executeOnSeparateThread:默认true,表示基准在独立的线程上执行,对应底层InProcessEmitSettings.ExecuteOnSeparateThread设置(见 src/BenchmarkDotNet/Toolchains/InProcess/InProcessSettings.cs)。
此外,InProcessAttribute允许标注在类或程序集(Assembly)级别,使用InProcessToolchainType.Auto时还会在未显式设置 Job Id 的情况下自动追加InProcess作为 Job 标识。
完整示例剖析:进程内 vs 进程外同台竞技
官方示例 samples/BenchmarkDotNet.Samples/IntroInProcess.cs 展示了最有价值的用法:在同一个 Benchmark 类里注册两个 Job,一个走默认的进程外工具链,一个走InProcessEmitToolchain,从而直接对比两者差异。
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Configs; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Order; using BenchmarkDotNet.Toolchains.InProcess.Emit; using System.Runtime.CompilerServices; namespace BenchmarkDotNet.Samples { [Config(typeof(Config))] [Orderer(SummaryOrderPolicy.FastestToSlowest)] [MemoryDiagnoser] public class IntroInProcess { private class Config : ManualConfig { public Config() { AddJob(Job.MediumRun .WithLaunchCount(1) .WithId("OutOfProc")); AddJob(Job.MediumRun .WithLaunchCount(1) .WithToolchain(InProcessEmitToolchain.Default) .WithId("InProcess")); } } [Benchmark(Description = "new byte[10kB]")] public byte[] Allocate() { return new byte[10000]; } [Benchmark(Description = "stackalloc byte[10kB]")] public unsafe void AllocateWithStackalloc() { var array = stackalloc byte[10000]; Consume(array); } [MethodImpl(MethodImplOptions.NoInlining)] private static unsafe void Consume(byte* input) { } } }这段示例的关键点:
- 自定义 Config 配置两个 Job:
Job.MediumRun代表中等规模的运行计划(默认包含一定数量的预热与测量迭代),两个 Job 都通过.WithLaunchCount(1)把启动次数压到 1——这是为了在对比时尽量减少无关变量。第一个 Job 不指定工具链(默认进程外),第二个通过.WithToolchain(InProcessEmitToolchain.Default)切换到进程内; InProcessEmitToolchain.Default:这是该工具链的默认单例实例,定义于 src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitToolchain.cs,它绑定当前运行时(RuntimeInformation.GetCurrentRuntime())与InProcessEmitSettings.Default。若需要自定义设置(如ExecuteOnSeparateThread = false),可通过InProcessEmitToolchain.From(new InProcessEmitSettings { ... })创建新实例;- 两个 Benchmark 都是典型的内存分配场景:
Allocate在托管堆上分配 10kB 字节数组,AllocateWithStackalloc则在栈上分配同样大小并通过[MethodImpl(MethodImplOptions.NoInlining)]的Consume防止死代码消除——这样配合[MemoryDiagnoser]可以直观看到堆分配(0 与 10000 bytes)与栈分配的差异; [Orderer(SummaryOrderPolicy.FastestToSlowest)]:结果按耗时从快到慢排序,方便直接比较 InProcess 与 OutOfProc 两个 Job 的表现。
需要说明的是,这个示例的核心目的并非"证明进程内更快",而是演示如何把进程内工具链接入既有基准配置体系。由于 InProcess 省去了进程启动与 JIT 预热等环节,其测量结果与真实进程外运行存在差异,请勿将两种 Job 的结果混为一谈用于绝对性能结论。
进程内工具链的两种实现:Emit 与 NoEmit
在 src/BenchmarkDotNet/Toolchains/InProcess 目录下,进程内工具链其实有两条实现路径:
| 工具链 | 命名空间 | 原理 | 适用场景 |
|---|---|---|---|
InProcessEmitToolchain | BenchmarkDotNet.Toolchains.InProcess.Emit | 通过System.Reflection.Emit在内存中动态生成可运行类型的 IL 并直接执行 | 常规 .NET 运行时(默认选择) |
InProcessNoEmitToolchain | BenchmarkDotNet.Toolchains.InProcess.NoEmit | 不发射 IL,直接基于反射(MethodInfo.Invoke等)驱动基准方法 | AOT 环境(如 NativeAOT),以及不支持动态代码生成的场景 |
从 InProcessAttribute.cs 的源码可以看到,Auto模式正是通过RuntimeInformation.IsAot在这两者之间自动抉择的。无论哪种实现,都共享同一个基类设置InProcessSettings(目前仅有ExecuteOnSeparateThread一个可配置项),以及同一套InProcessValidator校验逻辑。
底层执行机制:执行器与运行器
进程内工具链虽然"不生成可执行文件",但执行流程依然完整复刻了基准引擎的各个阶段(GlobalSetup、预热、迭代测量、GlobalCleanup 等)。其关键实现位于两个类:
1.InProcessEmitExecutor(执行器),见 src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitExecutor.cs:
- 当
executeOnSeparateThread为true时,会通过ExecutionContextHelper.SuppressFlow()压制执行上下文,启动一个专用后台线程,并配合BenchmarkSynchronizationContext确保基准在这个专用线程上运行而不是线程池线程; - 若基准方法标记了
[STAThread]且运行在 Windows 上,该线程会被设置为 STA 公寓状态(ApartmentState.STA),这解决了部分依赖 STA 线程模型的基准在进程内运行时的兼容问题; - 运行前后会临时将进程优先级提升为
High、当前线程优先级提升为Highest(并在 finally 中恢复原值),若 Job 配置了Affinity还会设置进程 CPU 亲和性——这些都是为了尽量贴近进程外运行时的测量条件; - 该工具链还支持在进程内路由 InProcess 诊断器(
CompositeInProcessDiagnoser相关逻辑),使 MemoryDiagnoser 等诊断器在进程内模式下依然可用。
2.InProcessEmitRunner(运行器),见 src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitRunner.cs:
- 首先调用
host.BeforeAnythingElseAsync()让诊断器先行挂钩(保证基于 JIT 的诊断器能捕获到第一次 JIT 编译); - 从构建产物中取出动态生成的类型(类型名前缀来自
RunnableConstants.EmittedTypePrefix),实例化后通过构造函数 out 参数拿到"欺骗 JIT"的委托(trickTheJit),再通过反射填充基准实例上的参数、[Params]参数以及标记了[BenchmarkCancellation]的CancellationToken字段/属性; - 随后构造
EngineParameters,把 GlobalSetup/GlobalCleanup/IterationSetup/IterationCleanup 及 Workload 等回调全部绑定到动态类型的方法上,交给标准的IEngine驱动完整的基准循环,最后host.ReportResults(results)回报结果; - 若基准抛出
OutOfMemoryException,运行器会给出专门的提示(指出基准可能存在内存泄漏并建议使用OperationsPerInvoke、IterationSetup、IterationCleanup消除副作用),这与进程外模式下由子进程自行处理异常的逻辑形成呼应。
从源码结构看,这一套设计让进程内模式尽可能复用了进程外模式的引擎与阶段逻辑,差异主要被封装在"如何构造可运行代码"这一层(Emit/NoEmit)以及"在哪里执行"(独立进程 vs 当前进程的专用线程)这两处。
环境一致性校验:InProcessValidator
进程内运行最大的风险是:基准 Job 中声明的环境特征(Platform、JIT、GC 模式等)必须与当前宿主进程实际环境一致,否则测量结果没有意义。为此,进程内工具链配套提供了 src/BenchmarkDotNet/Toolchains/InProcess/InProcessValidator.cs:
InProcessValidator.FailOnError:将环境不匹配视为错误并终止运行;InProcessValidator.DontFailOnError:只报告警告,不阻断运行。
其校验规则表(ValidationRules)覆盖了以下特征:
| 特征类别 | 具体特征 | 校验策略 |
|---|---|---|
| 环境 | Jit、Platform、GcMode.Server、GcMode.Concurrent、GcMode.CpuGroups、GcMode.NoAffinitize | 与实际环境比对,不一致即报错/警告 |
| 工具链 | Toolchain | 必须是InProcessEmitToolchain或InProcessNoEmitToolchain |
| 运行参数 | LaunchCount、RunStrategy、WarmupCount、IterationCount、IterationTime、InvocationCount、UnrollFactor等 | 不校验(进程内可自由设置) |
| 精度参数 | MaxRelativeError、MaxAbsoluteError、OutlierMode、MinIterationTime等 | 不校验 |
同时,校验器明确指出:进程内工具链目前不支持带参数的基准方法(Arguments)——若基准方法带有参数,会生成"Arguments are not supported by the InProcessToolchain yet, see #687 for more details"的校验错误。
官方示例 samples/BenchmarkDotNet.Samples/IntroInProcessWrongEnv.cs 演示了"故意制造环境不匹配"的用法:它通过Environment.Is64BitProcess反选平台(64 位进程却声明 X86,反之亦然)构造一个与当前环境必然冲突的 Job,并搭配AddValidator(InProcessValidator.DontFailOnError)让基准在警告模式下继续运行——这个示例常用于验证校验器确实在起作用,以及理解"进程内模式下声明的平台/架构实际不会被切换"这一关键事实。注意:进程内模式下声明了错误平台,基准依然会跑,但结果的合法性与代表性是存疑的,正式使用时应采用FailOnError或保证配置与宿主进程环境完全一致。
进程内模式的适用边界与注意事项
综合官方文档与源码,使用 InProcess 工具链前应明确以下边界:
- 适用场景:需要快速验证基准逻辑正确性、追求极短的总运行时间、在自编译/自研运行时上运行基准、或作为集成测试的一部分在测试进程内直接执行基准(仓库中大量集成测试如 tests/BenchmarkDotNet.IntegrationTests/InProcessEmitTest.cs、
CallerThreadTests、ValuesReturnedByBenchmarkTest都依赖进程内模式来避免子进程编排的复杂度); - 不适用/受限场景:带参数的基准方法(Arguments)不受支持;依赖进程级隔离的场景(如测量进程启动开销、测试不同 JIT/运行时切换、需要独立内存空间的场合)不应使用进程内模式;
- 环境一致性:进程内模式下 JIT、Platform、GC 配置无法被真正切换,必须通过
InProcessValidator校验或人工保证 Job 配置与宿主进程一致; - 结果解读:进程内模式的迭代耗时与内存分配测量仍然有效,但它缺少进程启动、运行时初始化等真实部署开销,与进程外结果属于不同测量口径。
更完整的工具链总览(包括多框架运行、自定义 CoreRun、NativeAOT、Wasm 等)可参考 docs/articles/configs/toolchains.md,其中也内嵌了本示例与IntroInProcessWrongEnv示例的文档页。
总结
InProcess 工具链是 BenchmarkDotNet 在"进程级隔离"与"极速/特殊运行时支持"之间提供的另一条路径:InProcessEmitToolchain通过动态发射 IL 在宿主进程内直接运行基准,InProcessNoEmitToolchain则基于反射服务于 AOT 等无法发射 IL 的环境。通过[InProcess]特性或Job.WithToolchain(InProcessEmitToolchain.Default)可以一键接入,配合InProcessValidator校验环境一致性、MemoryDiagnoser观察分配差异,你可以在不牺牲 BenchmarkDotNet 测量框架的前提下,获得接近零启动成本的基准运行体验——尤其适合作为 CI 中的快速冒烟基准,或在自研运行时上完成基准验证。
- 性能测试
- 开发工具
【免费下载链接】BenchmarkDotNet
Powerful .NET library for benchmarking
相关推荐
XMRig 内嵌基准测试与压力测试完全指南:--bench / --stress 实战与原理剖析
XMRig 内嵌基准测试与压力测试完全指南: bench / stress 实战与原理剖析 本篇技术指南围绕 XMRig 内置的嵌入式基准测试(Embedded
区块链高性能计算Terminal.Gui 性能基准测试完全指南:内存 Profiler、BenchmarkDotNet 与 CI 性能门禁
Terminal.Gui 性能基准测试完全指南:内存 Profiler、BenchmarkDotNet 与 CI 性能门禁 Terminal.Gui 是面向 .
UI组件跨平台桌面应用Garnet 基准测试指南:从 RESP 端到端压测到 BenchmarkDotNet 微基准与 Tsavorite 设备层
Garnet 基准测试指南:从 RESP 端到端压测到 BenchmarkDotNet 微基准与 Tsavorite 设备层 Garnet 仓库的 benchm
缓存KV存储后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考