fixture 设计
设计目标
同一个输入必须到达每个实现,而准备它的代价是否计入测量,必须由声明决定,而不是出于偶然。fixture 把发生在输入上的五件事(生成、标识、复制、准备、重置)分开,使运行器能把每一件都放在时钟的正确一侧。
数学背景
夹具是一个按数据集 和实现 应用的小型状态机:
两个等式表达了运行器所依赖的契约。
- 隔离。 如果某个实现修改了 ,下一次准备仍必须看到原始的 : 不得与 共享可变状态。这样每个批次都从相同的状态开始,并且在每个区组中被测量的工作负载都是 的同一个函数。
- 确定性。 只是上下文的函数,因此相等的上下文给出相等的输入和相等的指纹:。
运行器对两者都不检查;违反它们会使各区组测量不同的工作负载。
测量中的准备代价
设一次操作的代价为 ,一次准备的代价为 。对于含 次操作的批次,运行器报告 ,其中
SetupFrequency | IncludedInMeasurement | ExcludedFromMeasurement |
|---|---|---|
PerIteration | ||
PerSample, PerBatch | ||
| 长期存活 | (那一次准备落在一个早期的、被丢弃的批次中) |
按批次计入准备工作,测得的是取决于校准所选批次大小的摊销代价;只有按迭代计入,或批次大小固定时,才应计入准备工作。
设计决策
记录中的闭包,而不是 trait
问题。 各夹具的输入类型、准备后类型和策略各不相同,而且常常在测试中内联编写。选择。 Fixture 是带三个类型参数的函数记录。理由。 trait 需要为每个夹具定义一个类型,并且无法把 id、版本和策略作为值携带。
先克隆再准备
运行器总是在 prepare 之前调用 clone_input。Fixture::immutable 让两者都成为恒等函数,这对永不修改的值没有代价;可变输入则提供真正的副本。把克隆和准备分开,使夹具可以只复制一次输入并围绕它建立工作区,也使两者各自的代价都可见。
感知实现的准备
prepare 接收实现 id,因此它可以生成某个实现所期望的布局(打包、转置、填充)。这样准备工作就属于夹具,其计时遵循声明的策略,而不是隐藏在某个实现的负载中。
策略即数据
SetupPolicy 随每个观测(setup_timing)以及在事件流中被记录。读者无需阅读代码就能区分冷启动测量和热态测量。
正确性与不变量
materialize和fingerprint对每个数据集运行一次。prepare总是接收一个新的clone_input结果。- 每个短期存活的准备值都恰好被重置一次;长期存活的值在运行器的检查点处被重置并复用(参见 runner 设计)。
Fixture::immutable使用恒等函数和空操作的重置。
被否决的方案
- 让实现自行复制输入。 这样复制在某些实现中会被计时,而在另一些实现中不会。
- 单一的准备钩子。 无法区分生成(每个数据集一次)与准备(每个批次或每次迭代)。
边界
- 夹具并不强制隔离性或确定性;它只陈述契约。
WorkspaceScope是描述性的;运行器按实现和数据集为其缓存建立键。PerRun和PerDataset与PerImplementation一样,按实现分别准备。- 夹具不测量内存。