<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Benchmark on Niu Zhiwei 的个人技术博客</title><link>https://nzw921rx.github.io/nzw921rx-blog/tags/benchmark/</link><description>Recent content in Benchmark on Niu Zhiwei 的个人技术博客</description><generator>Hugo</generator><language>zh-CN</language><lastBuildDate>Mon, 10 Aug 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://nzw921rx.github.io/nzw921rx-blog/tags/benchmark/index.xml" rel="self" type="application/rss+xml"/><item><title>SeaTunnel 基准测试设计与实践</title><link>https://nzw921rx.github.io/nzw921rx-blog/posts/seatunnel-benchmark-design/</link><pubDate>Mon, 10 Aug 2026 00:00:00 +0800</pubDate><guid>https://nzw921rx.github.io/nzw921rx-blog/posts/seatunnel-benchmark-design/</guid><description>从一组真实结果出发，解释 SeaTunnelRow 微基准和单节点 Zeta Pipeline 基准的测量边界、指标含义、实现方法与理论依据。</description><content:encoded><![CDATA[<p><img alt="SeaTunnel JMH 基准测试汇总" loading="lazy" src="/nzw921rx-blog/posts/seatunnel-benchmark-design/jmh-summary.webp"></p>
<p><em>图 1：同一轮运行中的 JMH 汇总，包括完整 Pipeline 和 SeaTunnelRow 热点操作。</em></p>
<p><img alt="SeaTunnel Pipeline 吞吐与延迟结果" loading="lazy" src="/nzw921rx-blog/posts/seatunnel-benchmark-design/pipeline-results.webp"></p>
<p><em>图 2：五条 Pipeline 在相同负载下的吞吐、延迟、增长比例与有效样本。</em></p>
<h2 id="一先读懂这组结果">一、先读懂这组结果</h2>
<p>这轮测试使用 Java 8、4 GiB 堆、4 个 JVM 可见处理器和 4 个 Pipeline 并行度；每次作业处理 1,000,000 行，计划输入速率为 600,000 行/秒，Payload 为 256 个字符。</p>
<p>结果可以压缩成三点：</p>
<ol>
<li>五个场景的 Sink 吞吐都在 58.8 万到 59.1 万行/秒，P99 为 100 到 101 ms，Growth 为 0.99 到 1.00，75/75 个样本全部有效，说明当前负载下没有持续积累 backlog；</li>
<li>Pipeline JMH Score 的最大差距约为 0.87%，而本轮 Error 为 0.88% 到 1.38%，因此只能说没有观察到可区分的功能开销，不能说某个场景更快或“零开销”；</li>
<li>60 万行/秒是本轮固定负载，不是容量上限。真正的容量评估还需要提高输入速率并持续观察吞吐、P99 和延迟增长。</li>
</ol>
<p>因此，这组数字首先证明的是基准链路完整、当前负载可持续，并且能够对不同引擎能力进行同口径比较，而不是给 Zeta 下一个脱离环境的绝对性能结论。</p>
<h2 id="二为什么要同时做微基准和完整-pipeline">二、为什么要同时做微基准和完整 Pipeline</h2>
<p>SeaTunnel 的基准测试分为两层，两者回答的问题不同。</p>
<h3 id="seatunnelrow-微基准定位热点">SeaTunnelRow 微基准：定位热点</h3>
<p><code>SeaTunnelRowBenchmark</code> 关注 Source、Transform 和 Sink 热路径中的基础操作，包括 Row 创建、字段读取、复制、投影、Options 和大小计算。</p>
<table>
	<thead>
			<tr>
					<th>操作</th>
					<th style="text-align: right">Score（ops/ms）</th>
					<th style="text-align: right">CV</th>
					<th>主要含义</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>copyPlainRow</code></td>
					<td style="text-align: right">22,328.74</td>
					<td style="text-align: right">1.01%</td>
					<td>完整复制普通 Row</td>
			</tr>
			<tr>
					<td><code>copyProjectedPlainRow</code></td>
					<td style="text-align: right">59,769.89</td>
					<td style="text-align: right">1.57%</td>
					<td>只复制 8 个字段中的 4 个投影字段</td>
			</tr>
			<tr>
					<td><code>copyProjectedRowWithOptions</code></td>
					<td style="text-align: right">49,282.32</td>
					<td style="text-align: right">0.59%</td>
					<td>投影复制，同时处理 Options</td>
			</tr>
			<tr>
					<td><code>copyRowWithOptions</code></td>
					<td style="text-align: right">20,789.92</td>
					<td style="text-align: right">0.22%</td>
					<td>完整复制带 Options 的 Row</td>
			</tr>
			<tr>
					<td><code>copyRowWithTracePayload</code></td>
					<td style="text-align: right">20,499.68</td>
					<td style="text-align: right">0.27%</td>
					<td>完整复制带 Trace Payload 的 Row</td>
			</tr>
			<tr>
					<td><code>copyThenMutateCopiedOptions</code></td>
					<td style="text-align: right">7,305.63</td>
					<td style="text-align: right">1.01%</td>
					<td>复制后修改 Options</td>
			</tr>
			<tr>
					<td><code>createRowAndGetBytesSize</code></td>
					<td style="text-align: right">5,837.52</td>
					<td style="text-align: right">0.68%</td>
					<td>创建 Row 并计算大小</td>
			</tr>
			<tr>
					<td><code>createRowWithSetField</code></td>
					<td style="text-align: right">7,668.90</td>
					<td style="text-align: right">0.59%</td>
					<td>逐字段构建 Row</td>
			</tr>
			<tr>
					<td><code>getBytesSizeCached</code></td>
					<td style="text-align: right">254,677.57</td>
					<td style="text-align: right">1.13%</td>
					<td>读取已经缓存的大小</td>
			</tr>
			<tr>
					<td><code>readFields</code></td>
					<td style="text-align: right">28,347.47</td>
					<td style="text-align: right">0.47%</td>
					<td>读取并消费全部字段</td>
			</tr>
	</tbody>
</table>
<p><code>ops/ms</code> 表示每毫秒完成多少次操作，越大越好。例如 <code>copyPlainRow</code> 的 22,328.74 ops/ms 约等于每秒 2,233 万次操作。</p>
<p>这些数字主要用于同一方法跨版本比较，或者比较语义相近的操作。不能因为 <code>getBytesSizeCached</code> 比 <code>createRowAndGetBytesSize</code> 高很多，就直接得出某段完整业务链路快了相同比例：前者只是读取缓存，后者同时包含对象创建和首次大小计算，测量对象并不相同。</p>
<p>在语义接近的复制场景中可以看到，带 Options 的完整复制比普通复制低约 6.9%，加入 Trace Payload 后比普通复制低约 8.2%。这类差异能够提示优化方向，但最终是否影响真实任务，仍要回到完整 Pipeline 验证。</p>
<h3 id="zeta-pipeline-基准验证整体效果">Zeta Pipeline 基准：验证整体效果</h3>
<p>完整 Pipeline 启动真实的单节点 Zeta Master 和 Worker，并通过正常的 Client、配置解析、任务提交和调度链路执行有界作业。</p>
<div class="mermaid">flowchart LR
    JMH["JMH Trial"] --> Cluster["单节点 Zeta"]
    Cluster --> Client["Client 提交作业"]
    Client --> Source["定速 Source"]
    Source --> Transform["可选 Transform"]
    Transform --> Sink["黑洞 Sink"]
    Sink --> Result["吞吐、延迟、校验结果"]
</div>
<p>集群在 <code>Trial</code> 的 Setup 阶段启动，不进入计时区间。每次 Benchmark invocation 都会提交一个包含 1,000,000 行数据的完整作业，作业配置生成、提交、调度、Source、可选 Transform、Sink 和完成等待都在测量范围内。</p>
<p>这种设计比只调用一个内部方法更接近真实引擎，又通过内存 Source 和黑洞 Sink 去掉了外部系统波动，适合回答“引擎核心链路是否发生变化”。</p>
<h2 id="三设计背后的理论依据">三、设计背后的理论依据</h2>
<p>基准测试的可信度包含两个层面：一是结果是否具有统计上的可靠性，二是指标是否真正测量了我们关心的性能问题。前者要求正确处理 JVM 波动、独立样本和实验误差，后者要求明确吞吐与延迟的测量边界。</p>
<p>这套设计主要参考了三项性能评估研究：</p>
<table>
	<thead>
			<tr>
					<th>研究</th>
					<th>核心问题</th>
					<th>主要结论</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><em>Statistically Rigorous Java Performance Evaluation</em>，OOPSLA 2007</td>
					<td>Java 程序每次运行都可能不同，怎样得到可信结论？</td>
					<td>区分启动与稳态，以独立 JVM 运行作为重要实验单位，同时报告估计值和不确定性</td>
			</tr>
			<tr>
					<td><em>Rigorous Benchmarking in Reasonable Time</em>，ISMM 2013</td>
					<td>Build、JVM 和 Iteration 都存在波动，有限的实验时间应该怎样分配？</td>
					<td>识别不同层级的方差，通过定标实验把重复预算用于主要的不确定性来源</td>
			</tr>
			<tr>
					<td><em>Benchmarking Distributed Stream Data Processing Systems</em>，ICDE 2018</td>
					<td>流处理系统中的吞吐和延迟应该从哪里开始测量？</td>
					<td>使用开环负载和事件生成时间，结合 backlog 与尾延迟判断可持续吞吐</td>
			</tr>
	</tbody>
</table>
<p>三项研究解决的不是三个孤立场景，而是基准测试中的三个连续问题：样本是否可信、实验预算是否有效，以及测量对象是否正确。</p>
<h3 id="jvm-性能评估从重复运行到可信样本">JVM 性能评估：从重复运行到可信样本</h3>
<p>Java 程序的性能不是一个固定常数。即使代码、参数和机器完全相同，JIT 编译时机、GC、线程调度、堆布局和操作系统扰动仍会让每次运行产生差异。</p>
<p>因此，只取多次运行中的最好值并不能代表程序的典型性能。<code>best of N</code> 实际回答的是“这 N 次运行中最幸运的一次有多快”。随着 N 增大，抽到极端好运样本的概率也会增加，最终结果会系统性地偏向波动更大的实现。</p>
<p>论文进一步区分了启动性能和稳定态性能。启动性能关注 JVM 启动、类加载、初始化和任务执行的总成本；稳定态性能关注程序完成初始化和主要 JIT 编译后的长期执行能力。两者测量的对象不同，不能混在同一个数字中。</p>
<p>同一个 JVM 内的 Iteration 共享 JIT Profile、已编译代码、堆状态和 GC 历史，因此不能把它们全部当成彼此独立的 JVM 实验。独立 JVM invocation，也就是 JMH 中的 Fork，是 Java 性能评估中不能忽略的实验层级。可信结果还需要同时报告估计值和不确定性，而不是只展示一个最好值或平均值。</p>
<h3 id="实验预算重复应该放在哪一层">实验预算：重复应该放在哪一层</h3>
<p>增加运行次数并不一定能够等比例提高结论的可信度。性能实验通常包含多个嵌套层级：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Build
</span></span><span style="display:flex;"><span>  └─ JVM Execution / Fork
</span></span><span style="display:flex;"><span>       └─ Measurement Iteration
</span></span><span style="display:flex;"><span>            └─ Pipeline Invocation
</span></span></code></pre></div><p>每个层级都可能引入不同的变化。重新 Build 可能改变代码布局，重新启动 JVM 会重新经历 JIT 和堆状态变化，同一个 JVM 内的 Iteration 则主要受到局部运行时扰动。</p>
<p>如果主要方差来自不同 Fork，那么把每个 Fork 中的 Iteration 从 5 次增加到 50 次，也无法消除 Fork 之间的差异；增加独立 Fork 可能更有价值。如果 Build 本身也会显著影响结果，就还需要把重复提升到 Build 层级。</p>
<p>论文提出先做定标实验：保留各层原始样本，估计 Build、Fork 和 Iteration 的方差贡献与时间成本，再把有限预算分配给主要的不确定性来源。因此，固定的 Fork 和 Iteration 次数只能作为起点，不能被当成适用于所有 Benchmark 的永久配置。</p>
<p>这里还需要区分随机误差和系统偏差。增加重复可以帮助估计随机误差，却不能修复测试顺序、机器状态或环境差异造成的系统偏差。如果 baseline 总是在机器空闲时运行，而 candidate 总是在机器繁忙时运行，再多样本也只会更精确地描述一个有偏实验。</p>
<h3 id="流处理测量从测得稳定到测量正确">流处理测量：从“测得稳定”到“测量正确”</h3>
<p>前两项研究关注怎样可靠地估计一个指标，第三项研究则进一步追问：这个指标是否真正代表了流处理系统的性能。</p>
<p>传统闭环负载生成器通常在上一批数据处理完成后再发送下一批。当系统变慢时，生成器也会跟着降低发送速度，尚未进入系统的数据等待时间不会被记录。最终可能出现一种反直觉现象：系统已经严重过载，报告中的内部处理延迟却仍然很低。这就是 Coordinated Omission。</p>
<p>为避免这个问题，输入速率必须独立于系统的完成速度，延迟也应从事件原本计划产生的时间开始计算。否则，Source 之前的排队会被排除，测量结果只能反映引擎摄入数据之后的处理时间，无法表达事件从应该产生到最终完成的真实等待。</p>
<p>这也引出了可持续吞吐与瞬时峰值的区别。可持续吞吐不是系统短时间内达到的最大速度，而是系统能够长期处理，同时不持续积累 backlog 和 Event-time Latency 的最高输入速率。吞吐、数据完整性、尾延迟和延迟增长必须一起解释，任何一个单独使用都不足以证明系统能够稳定承载当前负载。</p>
<h3 id="三项研究形成的方法论">三项研究形成的方法论</h3>
<p>三项研究共同形成了一条完整的论证链：先定义要评估的是启动、稳态、热点方法还是完整系统；再识别真正的独立实验单位和主要方差来源；最后确认吞吐与延迟的起止边界是否代表用户真正关心的问题。</p>
<p>统计方法无法修复错误的测量边界，正确的测量边界也不能代替独立重复和方差分析。只有两者同时成立，Benchmark 才能从一组运行数字变成可以支持工程判断的性能证据。</p>
<h2 id="四这些理论如何落到-zeta-pipeline">四、这些理论如何落到 Zeta Pipeline</h2>
<p>前一节确定了三项基本原则：保留独立 JVM 样本、把重复预算放在正确层级，并使用开环负载测量真实排队时间。本节不再重复这些概念，只说明它们怎样落实到 Zeta Pipeline 基准中。</p>
<h3 id="benchmarksource-记录计划生成时间">BenchmarkSource 记录计划生成时间</h3>
<p>Source 根据绝对时间安排每条记录的计划生成时间：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>scheduledTime = startTime
</span></span><span style="display:flex;"><span>              + wholeSeconds × 1000
</span></span><span style="display:flex;"><span>              + remainder × 1000 / rate
</span></span></code></pre></div><p>计划时间不会因为上一条记录处理变慢而暂停。记录到达 Sink 后计算：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>event-time latency = Sink 接收时间 - 计划生成时间
</span></span></code></pre></div><p>因此，引擎暂时无法及时处理所产生的排队，会直接进入 P50、P95 和 P99，而不会被 Source 自身降速隐藏。</p>
<h3 id="确定性组件隔离引擎开销">确定性组件隔离引擎开销</h3>
<p>测试组件尽量不引入随机业务行为：</p>
<ul>
<li>Source 生成固定数量、固定 Payload 的内存数据；</li>
<li>Transform 执行固定次数的 hash 工作，并输出可校验 checksum；</li>
<li>Sink 不访问外部系统，只统计行数、吞吐、延迟分布和 checksum；</li>
<li>每条 Pipeline 都保持相同的数据量、并行度和资源配置，只切换目标能力。</li>
</ul>
<p>五个场景分别覆盖基础数据链路、Transform、实时忙碌度观测、StainTrace 以及两者组合。这样可以在工作负载保持一致的前提下，观察不同引擎能力带来的增量开销。</p>
<h3 id="正确性校验决定样本是否有效">正确性校验决定样本是否有效</h3>
<p>性能很快但少处理了数据，是无效结果。每次完整作业都会检查：</p>
<ul>
<li><code>processed_rows</code> 是否等于 <code>expected_rows</code>；</li>
<li>基础 Source → Sink 场景的 checksum 是否为 0；</li>
<li>含 Transform 的场景 checksum 是否非 0；</li>
<li>延迟百分位是否落入统计范围，而不是被 overflow bucket 截断；</li>
<li>P99 和延迟增长是否满足当前基准的保护条件。</li>
</ul>
<p>只有数据完整、Transform 确实执行并且延迟可解释，样本才会进入最终报告。</p>
<h3 id="fork预热与机器可读结果">Fork、预热与机器可读结果</h3>
<p>公共 JMH 配置使用 3 个 Fork、每个 Fork 3 次预热和 5 次测量。Fork 提供相互独立的 JVM 进程，Warmup 结果不参与最终聚合，Measurement 才用于形成报告。</p>
<p>每个 Measurement Iteration 内可以完成多次完整 Pipeline invocation，所以报告中的 75/75 不是 75 行数据，也不等于 15 个 JMH Iteration，而是 Measurement 阶段实际完成的 75 个 Pipeline 作业样本。</p>
<p>除了 Markdown 汇总，测试还保留原始 JMH JSON、每次 Pipeline JSON、标准化指标和环境元数据。Commit、JDK、JVM 参数、运行器镜像、CPU、内核、内存和负载参数共同决定两组结果是否具有可比性。</p>
<p><code>3 Fork × 5 Measurement</code> 是当前阶段的初始预算，而不是永久配置。托管运行器上的结果首先用于观察；后续还需要在固定机器上积累历史分布，再根据真实方差决定重复次数、重跑规则和性能回归阈值。</p>
<h2 id="五怎样读懂两套数字">五、怎样读懂两套数字</h2>
<h3 id="jmh-score-和-pipeline-throughput-为什么不同">JMH Score 和 Pipeline Throughput 为什么不同</h3>
<p>Pipeline 类声明每次 invocation 包含 1,000,000 个逻辑操作，因此 JMH 会把完整作业耗时换算为 <code>ops/s</code>：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>JMH Score ≈ 1,000,000 / 完整作业耗时
</span></span></code></pre></div><p>这里的完整作业耗时包括配置生成、提交、调度、Source、Transform、Sink 和结束等待。</p>
<p>Pipeline JSON 的吞吐边界更窄：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Pipeline Throughput
</span></span><span style="display:flex;"><span>    = processed_rows / (Sink 最后一条接收时间 - 第一条接收时间)
</span></span></code></pre></div><p>它只观察 Sink 实际接收数据的区间，不包含作业提交和收尾固定成本。因此图中 JMH Score 约为 45.4 万到 45.8 万 ops/s，而 Pipeline Throughput 约为 58.8 万到 59.1 万行/秒。这不是两套结果矛盾，而是测量边界不同。</p>
<h3 id="scoreerrorcv-和-unit">Score、Error、CV 和 Unit</h3>
<table>
	<thead>
			<tr>
					<th>字段</th>
					<th>含义</th>
					<th>使用方式</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>Score</code></td>
					<td>JMH 对当前方法吞吐的估计值</td>
					<td>吞吐模式下越大越好</td>
			</tr>
			<tr>
					<td><code>Error</code></td>
					<td>当前运行内部置信区间半宽占 Score 的比例</td>
					<td>区间可近似理解为 <code>Score × (1 ± Error%)</code></td>
			</tr>
			<tr>
					<td><code>CV</code></td>
					<td>样本标准差除以样本均值</td>
					<td>越低表示本轮样本越集中</td>
			</tr>
			<tr>
					<td><code>Unit</code></td>
					<td>指标单位</td>
					<td>Pipeline 为 <code>ops/s</code>，Row 微基准为 <code>ops/ms</code></td>
			</tr>
	</tbody>
</table>
<p><code>Error</code> 和 CV 只能描述本次运行内部的波动，不包含不同宿主机、不同 CPU 调度状态和不同系统负载之间的漂移。比较两个版本时，不能只因为 Score 相差 1% 就认定发生了回归。</p>
<h3 id="p50p95p99maxgrowth-和-valid">P50、P95、P99、Max、Growth 和 Valid</h3>
<ul>
<li>P50：一半记录的延迟不超过该值，代表典型体验；</li>
<li>P95 / P99：观察尾部记录，能更早暴露排队、暂停和抖动；</li>
<li>Max：最慢记录，但对单个异常值更敏感，应和百分位一起看；</li>
<li>Growth：比较前后半段 P99，判断 backlog 是否持续累积；</li>
<li>Valid：Measurement 阶段完整、可持续且没有百分位溢出的样本数量。</li>
</ul>
<p>其中 Growth 的计算方式是：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Growth = (后半段 P99 + 1) / (前半段 P99 + 1)
</span></span></code></pre></div><p>吞吐告诉我们单位时间完成多少工作，延迟告诉我们每条记录等了多久，Growth 告诉我们这种等待是否还在恶化。三者必须结合，任何一个单独使用都可能产生错误结论。</p>
<h2 id="六从基准结果看-zeta-的性能表现">六、从基准结果看 Zeta 的性能表现</h2>
<p>在 4 个 JVM 可见处理器和 4 GiB 堆的资源条件下，Zeta 启动真实的单节点 Master 和 Worker，经过作业提交、调度以及完整的 <code>Source -&gt; Transform -&gt; Sink</code> 链路后，Sink 吞吐仍然保持在每秒 58.8 万到 59.1 万行，P99 约为 100 ms，75/75 个测量样本全部有效。</p>
<p>五条 Pipeline 的 Growth 都接近 1，说明在每秒 60 万行的计划输入下，系统没有持续积累延迟。加入 Transform、实时可观测性和 StainTrace 后，性能差异也没有超出本轮测量误差。它不能证明这些能力“零开销”，但至少说明当前开销没有对核心链路造成可识别的性能下降。</p>
<p>这组结果不是 Zeta 的容量上限，也不包含真实 Connector、多节点网络和 Checkpoint 等成本。不过在当前测量范围内，Zeta 已经表现出较高的吞吐能力、稳定的尾延迟和良好的功能开销控制。对于数据库、消息队列、数据仓库和数据湖之间的批流一体数据同步，SeaTunnel Zeta 是一款值得优先考虑的高性能引擎。</p>
<h2 id="七思考与总结">七、思考与总结</h2>
<p>设计基准测试的第一步不是立即编写 JMH 方法，而是先理解基准测试究竟要证明什么，并通过成熟研究掌握启动与稳态、独立样本、分层方差、开环负载和可持续吞吐等基本概念。理论的作用不是让测试看起来更复杂，而是帮助我们避免从错误的测量方式中得到精确但无意义的数字。</p>
<p>明确问题之后，需要固定计时边界、工作负载和资源条件，并使用确定性数据验证处理结果是否完整。只有先证明数据没有丢失、目标逻辑确实执行，吞吐和延迟才有讨论价值。</p>
<p>最后，应当分离预热与正式测量，保留独立重复，同时观察吞吐、尾延迟、Growth 和不确定性。托管运行器上的单次结果适合发现信号，不适合立即成为性能门禁；只有积累固定环境下的历史分布，才能进一步定义可靠的回归阈值。</p>
<p>基准测试的价值不在于展示一次漂亮的数字，而在于建立一套可以重复运行、解释性能变化并长期发现回归的证据体系。</p>
]]></content:encoded></item><item><title>从 Java 微基准到流处理系统：三篇性能评估论文精读与 JMH 实战</title><link>https://nzw921rx.github.io/nzw921rx-blog/posts/rigorous-java-stream-benchmarking/</link><pubDate>Fri, 31 Jul 2026 12:00:00 +0800</pubDate><guid>https://nzw921rx.github.io/nzw921rx-blog/posts/rigorous-java-stream-benchmarking/</guid><description>沿着“测量对象、实验单位、重复预算、结果解释”四个问题精读三篇性能评估论文，再从源码层解释 JMH 的生成代码、Fork、Warmup、Measurement 与结果聚合，并给出四个可落地的微基准场景。</description><content:encoded><![CDATA[<p>性能测试最容易制造的一种错觉是：数字越精确，结论就越可靠。</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>baseline    102.0 ns/op
</span></span><span style="display:flex;"><span>candidate    98.5 ns/op
</span></span></code></pre></div><p>这两行最多说明“某两次测量得到两个数字”，还不能证明 candidate 真快了 3.5 ns。我们至少还需要知道：</p>
<ul>
<li>测量的是冷启动、稳定态，还是某个随机的 JVM 生命周期位置？</li>
<li>样本来自多个独立 JVM，还是同一个 JVM 中高度相关的 Iteration？</li>
<li>差异来自代码，还是 JIT、GC、进程布局、机器漂移与测试顺序？</li>
<li>3.5 ns 即使真实存在，对实际系统是否有意义？</li>
</ul>
<p>到了分布式流处理系统，问题又多一层：如果事件已经在系统外排队 30 秒，而指标只从 Source 接收到事件后开始计时，那么“P99 延迟 20 ms”可能完全正确，也完全没有用户价值。</p>
<p>本文只做三件事：</p>
<ol>
<li>精读三篇论文，讲清它们分别解决了什么问题、怎样解决、用什么证据论证，以及结论的边界；</li>
<li>从源码层解释 JMH 怎样运行，再用四个具体场景讲清它的正确用法；</li>
<li>总结我对“可信性能证据”的理解。</li>
</ol>
<p>三篇论文不是三份互不相关的摘要，而是依次补上性能测试中的三个缺口：</p>
<table>
	<thead>
			<tr>
					<th>论文</th>
					<th>它追问的核心问题</th>
					<th>给出的主要答案</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>2007，Statistically Rigorous Java Performance Evaluation</td>
					<td>Java 程序每次运行都不同，应该怎样得到可信结果？</td>
					<td>区分启动与稳定态；以独立 JVM 运行作为重要实验单位；报告均值与置信区间，不取最好值</td>
			</tr>
			<tr>
					<td>2013，Rigorous Benchmarking in Reasonable Time</td>
					<td>严谨测试需要大量重复，怎样把时间花在真正产生不确定性的层级？</td>
					<td>识别 Build、Execution、Iteration 的分层方差；先定标，再分配重复预算；报告效应量区间</td>
			</tr>
			<tr>
					<td>2018，Benchmarking Distributed Stream Data Processing Systems</td>
					<td>流系统中的吞吐和延迟到底从哪里开始、到哪里结束？</td>
					<td>分离 Driver 与 SUT；使用外部队列和生成时间；测 Event-time Latency 与 Sustainable Throughput</td>
			</tr>
	</tbody>
</table>
<p>贯穿全文的核心思想只有一句话：</p>
<blockquote>
<p>可信 Benchmark 不是“多跑几次”，而是先定义测量对象，再找到独立实验单位，最后用与问题匹配的统计方法表达不确定性。</p>
</blockquote>
<h2 id="一三篇论文精读从测得准到测得对">一、三篇论文精读：从“测得准”到“测得对”</h2>
<h3 id="11-2007java-性能不是一个常数">1.1 2007：Java 性能不是一个常数</h3>
<p>论文：<a href="https://dri.es/files/oopsla07-georges.pdf">Statistically Rigorous Java Performance Evaluation</a>，Andy Georges、Dries Buytaert、Lieven Eeckhout，OOPSLA 2007。</p>
<h4 id="它要解决什么问题">它要解决什么问题</h4>
<p>Java 程序的执行时间会被很多运行时因素影响：</p>
<ul>
<li>JIT 的采样与编译时机；</li>
<li>线程调度；</li>
<li>GC 发生的时间与堆布局；</li>
<li>操作系统中断、缓存与其他机器状态。</li>
</ul>
<p>因此，即使代码、参数和机器都不变，多次运行也会得到不同结果。论文真正质疑的不是“有噪声”这件事，而是当时大量研究处理噪声的方式没有统计含义。</p>
<p>作者调查了 50 篇 Java 性能论文：</p>
<ul>
<li>16 篇没有说明实验方法；</li>
<li>10 篇报告最好值，8 篇报告平均值；</li>
<li>中位数与第二好值各有 4 篇，最差值有 3 篇；</li>
<li>只有 4 篇报告置信区间。</li>
</ul>
<p><code>best of N</code> 看似在排除机器噪声，实际回答的是“这 N 次中最幸运的一次有多快”。N 越大，遇到极端好运样本的机会越大，所以 <code>best of 3</code> 与 <code>best of 30</code> 甚至不是同一个估计目标。</p>
<p>假设两个实现的测量结果如下：</p>
<table>
	<thead>
			<tr>
					<th>实现</th>
					<th style="text-align: right">大多数运行</th>
					<th style="text-align: right">偶然出现的最好值</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>A</td>
					<td style="text-align: right">约 100 ms，波动很小</td>
					<td style="text-align: right">98 ms</td>
			</tr>
			<tr>
					<td>B</td>
					<td style="text-align: right">约 103 ms，波动很大</td>
					<td style="text-align: right">91 ms</td>
			</tr>
	</tbody>
</table>
<p>取最好值会宣布 B 获胜；但如果明天随机运行一次，A 更可能更快。最好值偏爱“容易偶尔走运”的实现，而不是典型情况下更快的实现。</p>
<h4 id="它怎样解决">它怎样解决</h4>
<p>论文先把两个经常混在一起的问题拆开。</p>
<p>启动性能关注 JVM 启动、类加载、初始化、JIT 与任务执行的总成本。实验方法是：</p>
<ol>
<li>启动多个独立 JVM；</li>
<li>每个 JVM 只执行一次任务；</li>
<li>将每次 JVM invocation 视为一个观测；</li>
<li>在这些观测上计算均值与置信区间。</li>
</ol>
<p>稳定态性能关注长时间运行程序在初始化之后的执行能力。论文的方法是：</p>
<ol>
<li>启动多个独立 JVM；</li>
<li>每个 JVM 执行多个 Iteration；</li>
<li>在每个 JVM 内找到作者认为已经稳定的窗口，并计算该 JVM 的平均值；</li>
<li>最后在多个 JVM 平均值之间计算总体均值与置信区间。</li>
</ol>
<p>这里最关键的不是“预热多少次”，而是实验单位：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Fork 1: Iteration 1 ... Iteration k  -&gt;  Fork 1 的稳定态均值 m1
</span></span><span style="display:flex;"><span>Fork 2: Iteration 1 ... Iteration k  -&gt;  Fork 2 的稳定态均值 m2
</span></span><span style="display:flex;"><span>...
</span></span><span style="display:flex;"><span>Fork n: Iteration 1 ... Iteration k  -&gt;  Fork n 的稳定态均值 mn
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>最终在 m1, m2, ..., mn 上估计均值与不确定性
</span></span></code></pre></div><p>同一个 JVM 内的 Iteration 共享 JIT Profile、已编译代码、堆与 GC 历史，因此不能简单地把 5 个 Fork × 10 个 Iteration 宣称为 50 次彼此独立的 JVM 实验。</p>
<p>论文使用置信区间表达随机误差。一个常见的小样本均值区间可以简写为：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>CI = x̄ ± t(0.975, n - 1) × s / √n
</span></span></code></pre></div><p>每个符号的含义是：</p>
<ul>
<li><code>CI</code>：这里是 95% 置信区间；</li>
<li><code>x̄</code>：n 个独立实验单位的样本均值；</li>
<li><code>s</code>：这 n 个样本的样本标准差；</li>
<li><code>n</code>：独立实验单位的数量，在这里通常应理解为独立 JVM invocation 的数量，而不是方法调用次数；</li>
<li><code>√n</code>：样本数量 n 的平方根；</li>
<li><code>t(0.975, n - 1)</code>：自由度为 <code>n - 1</code> 的 t 分布 97.5% 分位点，用来形成双侧 95% 区间。</li>
</ul>
<p>95% 置信区间的严格含义不是“真实均值有 95% 概率位于这个已经算出的区间内”，而是：如果按照同一规则反复做实验并构造区间，长期来看约 95% 的区间会覆盖真实均值。</p>
<h4 id="它怎样论证">它怎样论证</h4>
<p>作者没有只做一个玩具算例，而是构造了较大的实验矩阵：</p>
<ul>
<li>5 种 Jikes RVM 垃圾收集策略；</li>
<li>14 个 Benchmark，其中 7 个来自 SPECjvm98，7 个来自 DaCapo；</li>
<li>每个 Benchmark 从最小可运行堆开始，到 6 倍最小堆结束，以 0.25 倍为步长，共 21 个堆大小；</li>
<li>3 台不同硬件平台。</li>
</ul>
<p>论文使用 30 个 JVM invocation 分析启动性能；稳定态实验则使用多个 JVM，并在每个 JVM 内执行多个 Iteration。作者把常见的 best、second best、mean、median、worst 方法与更严格的置信区间比较结论进行对照。</p>
<p>在这套特定实验中：</p>
<ul>
<li>启动性能的常见简化方法，误导性结论最高达到约 16%；</li>
<li>某些方法给出相反优劣方向的错误结论超过 3%；</li>
<li>稳定态比较在最小关注差异为 1% 时，误导性结论超过 20%；</li>
<li>即使使用 Replay Compilation 控制一部分 JIT 随机性，误导也没有完全消失。</li>
</ul>
<p>这些百分比不能脱离论文的 GC、堆、硬件与阈值设置外推；它们真正证明的是：错误的汇总方法并非只在理论上有风险，而是足以改变工程结论。</p>
<p>论文还发现，达到同一精度所需的样本数会随 Benchmark、GC 与堆大小显著变化。有的组合不到 10 次就得到很窄的区间，有的组合做满 30 次后区间仍然较宽。因此，“统一跑 5 次”或“达到 30 次就可靠”都没有理论依据。</p>
<h4 id="这篇论文的核心结论">这篇论文的核心结论</h4>
<p>我认为它最重要的贡献不是某个统计公式，而是确立了四个基本原则：</p>
<ol>
<li>启动性能与稳定态性能是两个不同的问题；</li>
<li>JVM invocation 是 Java 性能评估中不可忽略的独立层级；</li>
<li>最好值、最差值不能代表典型性能；</li>
<li>性能结果必须同时报告估计值与不确定性。</li>
</ol>
<h4 id="它没有解决什么">它没有解决什么</h4>
<p>论文用“最近一段 Iteration 的变异系数 CoV 低于阈值”寻找稳定窗口。CoV 是标准差除以均值，它只能说明最近的点挤得是否紧，不能证明：</p>
<ul>
<li>序列没有缓慢上升或下降；</li>
<li>相邻 Iteration 彼此独立；</li>
<li>不存在奇偶交替或周期；</li>
<li>后面不会发生 Deoptimization 或状态切换。</li>
</ul>
<p>换句话说，低波动不等于稳态。这正是第二篇论文要修正的地方。</p>
<h3 id="12-2013严谨不等于把每一层都机械地跑很多次">1.2 2013：严谨不等于把每一层都机械地跑很多次</h3>
<p>论文：<a href="https://dl.acm.org/doi/10.1145/2491894.2464160">Rigorous Benchmarking in Reasonable Time</a>，Tomas Kalibera、Richard Jones，ISMM 2013（<a href="https://www.researchgate.net/publication/257193308_Rigorous_Benchmarking_in_Reasonable_Time">可访问全文</a>）。</p>
<h4 id="它要解决什么问题-1">它要解决什么问题</h4>
<p>2007 年论文告诉我们要运行多个 JVM、多个 Iteration，并报告区间。但新的现实问题是：如果 Build、JVM Execution、Iteration 都重复很多次，实验成本会迅速爆炸。</p>
<p>作者调查了 2011 年 PLDI、ASPLOS、ISMM、TOPLAS 与 TACO 的 122 篇论文：</p>
<ul>
<li>90 篇测量了执行时间；</li>
<li>其中 71 篇没有报告任何变异信息；</li>
<li>65 篇报告了执行时间比值；</li>
<li>只有 3 篇尝试为比值给出置信区间。</li>
</ul>
<p>论文认为，研究者不是不知道应该严谨，而是不知道：</p>
<ul>
<li>哪一层必须重复；</li>
<li>每层重复多少次；</li>
<li>多花一分钟应该增加 Build、Execution，还是 Iteration；</li>
<li>怎样直接表达“新版本究竟快了多少”。</li>
</ul>
<h4 id="它怎样解决-1">它怎样解决</h4>
<p>第一步是区分三类变量：</p>
<table>
	<thead>
			<tr>
					<th>变量类型</th>
					<th>含义</th>
					<th>正确处理</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Controlled</td>
					<td>实验者主动固定或选择的变量，如 JVM 参数、堆大小</td>
					<td>明确记录并保持一致，或作为实验因素系统变化</td>
			</tr>
			<tr>
					<td>Random</td>
					<td>每次实验会随机变化的变量，如调度与运行时扰动</td>
					<td>通过独立重复估计其影响</td>
			</tr>
			<tr>
					<td>Uncontrolled</td>
					<td>实验期间近似固定、但实验者没有控制的变量，如代码布局偏置</td>
					<td>尽量控制；不能控制时将其随机化，否则会形成系统偏差</td>
			</tr>
	</tbody>
</table>
<p>统计方法可以描述随机误差，却无法自动修复系统偏差。如果 baseline 总在机器冷的时候跑、candidate 总在机器热的时候跑，再漂亮的置信区间也只是在精确描述一个有偏实验。</p>
<p>第二步是把实验看成嵌套层级：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Build 1
</span></span><span style="display:flex;"><span>  ├─ JVM Execution 1
</span></span><span style="display:flex;"><span>  │    ├─ Iteration 1
</span></span><span style="display:flex;"><span>  │    └─ Iteration 2
</span></span><span style="display:flex;"><span>  └─ JVM Execution 2
</span></span><span style="display:flex;"><span>       ├─ Iteration 1
</span></span><span style="display:flex;"><span>       └─ Iteration 2
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Build 2
</span></span><span style="display:flex;"><span>  └─ ...
</span></span></code></pre></div><p>如果只考虑 Fork 与 Iteration 两层，总体均值方差可以用下面的简化模型理解：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Var(x̄) ≈ σ²fork / F + σ²iter / (F × I)
</span></span></code></pre></div><p>符号含义如下：</p>
<ul>
<li><code>Var(x̄)</code>：最终均值估计的不确定性；</li>
<li><code>σ²fork</code>：不同 Fork 之间的方差分量；</li>
<li><code>σ²iter</code>：同一 Fork 内不同 Iteration 的方差分量；</li>
<li><code>F</code>：Fork 数量；</li>
<li><code>I</code>：每个 Fork 中参与测量的 Iteration 数量。</li>
</ul>
<p>这个式子揭示了一个非常实际的结论：</p>
<blockquote>
<p>增加 Iteration 只能缩小第二项，无法消除 Fork 之间的差异。</p>
</blockquote>
<p>如果主要噪声来自不同 JVM 进程，那么把每个 Fork 的 Iteration 从 5 增加到 50，收益可能很小；增加独立 Fork 才真正压缩主要不确定性。如果重新 Build 也会改变代码布局与性能，那么还需要在更高层加入 Build 方差，并重复 Build。</p>
<p>论文因此提出“定标实验”：</p>
<ol>
<li>第一次为某个 Benchmark、VM 与平台组合做较充分的嵌套重复；</li>
<li>估计每一层贡献的方差；</li>
<li>测量每一层重复的时间成本，例如启动 JVM、完成预热、重新构建；</li>
<li>在给定总时间预算下，把重复分配到“方差贡献大且单位成本合理”的层级；</li>
<li>只有当 Benchmark、VM 或平台明显变化时，才重新定标。</li>
</ol>
<p>第三步是修正“低 CoV 等于稳态”。作者区分：</p>
<ul>
<li>Initialized state：明显的初始化阶段已经结束；</li>
<li>Independent state：后续 Iteration 不仅看似平稳，而且没有明显自相关。</li>
</ul>
<p>他们结合 Run-sequence Plot、Lag Plot 与自相关函数 ACF 检查序列，而不是只看最近窗口的波动大小。</p>
<h4 id="它怎样论证-1">它怎样论证</h4>
<p>作者对 DaCapo 的多种 Benchmark、JVM 与平台组合执行最多 300 个 Iteration，并比较手工诊断、DaCapo Harness 启发式和 2007 年 CoV 方法。</p>
<p>下面是论文 Table 3 中 Platform P1 的三个典型例子：</p>
<table>
	<thead>
			<tr>
					<th>Benchmark</th>
					<th style="text-align: right">初始化完成</th>
					<th style="text-align: right">达到独立态</th>
					<th style="text-align: right">DaCapo Harness 选择</th>
					<th style="text-align: right">2007 CoV 方法选择</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>avrora9</td>
					<td style="text-align: right">2</td>
					<td style="text-align: right">128</td>
					<td style="text-align: right">4</td>
					<td style="text-align: right">1</td>
			</tr>
			<tr>
					<td>lusearch9</td>
					<td style="text-align: right">3</td>
					<td style="text-align: right">5</td>
					<td style="text-align: right">5</td>
					<td style="text-align: right">247</td>
			</tr>
			<tr>
					<td>xalan6</td>
					<td style="text-align: right">2</td>
					<td style="text-align: right">2</td>
					<td style="text-align: right">29</td>
					<td style="text-align: right">无法收敛</td>
			</tr>
	</tbody>
</table>
<p>三行数据分别说明：</p>
<ul>
<li><code>avrora9</code> 中，CoV 方法在初始化完成之前就宣布稳定；</li>
<li><code>lusearch9</code> 第 5 轮已经达到论文判定的独立态，CoV 方法却预热到第 247 轮；</li>
<li><code>xalan6</code> 第 2 轮已经独立，CoV 方法反而无法收敛。</li>
</ul>
<p>因此，固定 <code>Warmup(iterations = 5)</code> 只是配置，不是“已经进入稳定态”的证据；同样，无限增加 Warmup 也未必能得到独立样本。</p>
<p>论文还给出一个很重要、但经常被忽略的退路：如果 Benchmark 在合理时间内无法达到独立态，就不要让在线启发式在每次运行中随意挑不同窗口，而应在所有运行中选择同一个、已经完成初始化的生命周期位置。它测量的可能不是理想“峰值稳态”，但至少比较对象一致。</p>
<p>第四步是直接报告效应量区间。只说 <code>p &lt; 0.05</code> 只能说明数据不太支持“完全无差异”，不能回答差异是否重要。对于执行时间，更有用的表达是：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>candidate / baseline = 0.94，95% CI [0.92, 0.97]
</span></span></code></pre></div><p>这是一个教学示例，不是论文的实测数据。它表达的是：candidate 的执行时间估计为 baseline 的 94%，区间为 92% 到 97%；也就是性能改善大约在 3% 到 8% 之间。差异的方向、幅度和不确定性一次说清楚了。</p>
<h4 id="这篇论文的核心结论-1">这篇论文的核心结论</h4>
<p>第二篇论文把“多跑几次”推进成了“在哪一层多跑几次”：</p>
<ol>
<li>先控制或随机化系统偏差；</li>
<li>找到真正引入随机性的最高层；</li>
<li>用定标实验估计各层方差与成本；</li>
<li>把重复预算花在主要不确定性上；</li>
<li>报告性能变化的幅度与区间，而不只报告显著性。</li>
</ol>
<h4 id="它没有解决什么-1">它没有解决什么</h4>
<p>这套方法不是一组可以从论文直接复制的固定次数。作者明确强调：重复次数依赖 Benchmark、VM 与平台，论文表格中的数字不能原样套用到另一台机器。</p>
<p>它还需要几个前提：</p>
<ul>
<li>分层结构和方差模型与实际实验大致匹配；</li>
<li>定标之后，各层成本与方差没有发生根本变化；</li>
<li>实验者愿意查看时间序列并判断初始化与独立性；</li>
<li>测量指标本身已经定义正确。</li>
</ul>
<p>最后一个前提非常关键。统计方法能告诉我们一个指标估计得有多准，却不能告诉我们这个指标是否代表用户真正关心的问题。第三篇论文就从这里开始。</p>
<h3 id="13-2018流系统的最大输入速度不等于可持续吞吐">1.3 2018：流系统的“最大输入速度”不等于可持续吞吐</h3>
<p>论文：<a href="https://arxiv.org/pdf/1802.08496">Benchmarking Distributed Stream Data Processing Systems</a>，Jeyhun Karimov 等，ICDE 2018。</p>
<h4 id="它要解决什么问题-2">它要解决什么问题</h4>
<p>当时不少流处理 Benchmark 存在三类测量边界错误。</p>
<p>第一，Kafka、Redis 等外部组件可能先成为瓶颈，最后测到的是整套部署中最慢的外部组件，而不是流处理引擎。</p>
<p>第二，Driver 与 SUT（System Under Test）混在一起。不同系统使用自己的内部指标，延迟起点、吞吐口径和结果数量都不一致，横向比较失去共同尺度。</p>
<p>第三，只测 Processing-time Latency。事件在 Source 之前的等待时间被排除后，背压越严重，SUT 反而可能摄入得越慢、内部处理延迟越稳定；外部队列却在持续增长。</p>
<p>这就是 Coordinated Omission：测量节奏跟着被测系统变慢，恰好漏掉系统无法及时服务的那部分等待。</p>
<h4 id="它怎样解决-2">它怎样解决</h4>
<p>论文把 Driver 与 SUT 完全分离：</p>
<div class="mermaid">flowchart LR
    D["Driver<br/>固定速率生成并打时间戳"] --> Q["外部内存队列<br/>观察 backlog"]
    Q --> S["SUT<br/>Source → Window → Sink"]
    S --> O["Observer<br/>记录输出时间与吞吐"]
    D -. "生成时间" .-> O
    Q -. "队列长度" .-> O
</div>
<p>具体做法包括：</p>
<ul>
<li>Driver 在独立机器上按固定速率生成数据，不等待上一条处理完成；</li>
<li>每条事件在生成时记录 Event Time；</li>
<li>Driver 与 SUT Source 之间放置内存队列，吸收瞬时摄入波动并暴露 backlog；</li>
<li>吞吐从 Driver/队列侧测量，延迟在输出侧根据原始时间戳计算；</li>
<li>Driver 不与 SUT 共用机器，避免生成负载抢占被测系统资源。</li>
</ul>
<p>这里需要区分两个延迟：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Event-time latency      = 输出时间 - 事件生成时间
</span></span><span style="display:flex;"><span>Processing-time latency = 输出时间 - SUT 摄入时间
</span></span></code></pre></div><p>前者包含 SUT 外部排队，更接近事件从产生到获得结果的等待；后者只描述事件进入 SUT 之后的处理时间。两者都有诊断价值，但不能互相替代。</p>
<p>窗口算子的输出由多条输入共同产生，不能随便选择一条输入的时间戳。论文定义：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>窗口输出的 event time = 所有贡献输入中最大的 event time
</span></span><span style="display:flex;"><span>窗口输出延迟 = 输出时间 - 窗口输出的 event time
</span></span></code></pre></div><p>其中：</p>
<ul>
<li>“贡献输入”是实际参与该聚合或 Join 输出的输入事件；</li>
<li>“最大的 event time”代表最后一个必要输入到达的逻辑时间；</li>
<li>“输出时间”是 SUT 产生结果的时刻。</li>
</ul>
<p>例如一个聚合结果由 event time 为 580、590、600 的三条输入构成，并在 610 输出，那么论文口径下的窗口输出延迟是 <code>610 - 600 = 10</code>。这样不会把窗口本身等待数据凑齐的完整跨度全部算成引擎处理延迟。</p>
<p>这个定义也有明确语义边界：它衡量“最后一个必要事件之后多久产出窗口结果”，不是每一条早到事件的等待时间。如果业务关心第一条事件进入窗口后等待了多久，还需要另加指标。</p>
<p>论文进一步定义 Sustainable Throughput：</p>
<blockquote>
<p>系统能够长期处理、且不会出现持续背压或 Event-time Latency 持续增长的最高输入速率。</p>
</blockquote>
<p>寻找方法不是让 Source 无限制拉取并记录瞬时峰值，而是：</p>
<ol>
<li>先给出明显高于系统能力的固定生成速率；</li>
<li>观察队列长度和 Event-time Latency 是否持续增长；</li>
<li>逐步降低生成速率；</li>
<li>找到系统可以长期维持、backlog 不再单调增长的最高速率。</li>
</ol>
<h4 id="它怎样论证-2">它怎样论证</h4>
<p>作者使用游戏业务启发的窗口聚合和窗口 Join，评估当时版本的 Storm 1.0.2、Spark 2.0.1 与 Flink 1.1.3，并改变：</p>
<ul>
<li>集群节点数；</li>
<li>窗口大小与滑动步长；</li>
<li>数据倾斜；</li>
<li>输入速率波动；</li>
<li>聚合与 Join 工作负载。</li>
</ul>
<p>最有说服力的证据不是三个系统的历史排行榜，而是过载实验中的指标分裂：</p>
<ul>
<li>SUT 启动背压后，摄入速率下降；</li>
<li>Processing-time Latency 可以保持相对稳定；</li>
<li>但事件继续堆积在外部队列中，Event-time Latency 持续上升。</li>
</ul>
<p>如果只看 SUT 内部处理时间，会误判系统仍然健康；外部时间戳和 backlog 才揭示真实的容量不足。论文也展示了不同系统在数据倾斜、突发流量、窗口聚合和窗口 Join 下呈现不同瓶颈，说明单一的“每秒多少条”无法概括流系统性能。</p>
<h4 id="这篇论文的核心结论-2">这篇论文的核心结论</h4>
<p>第三篇论文把问题从“数字是否稳定”推进到“数字是否测对了东西”：</p>
<ol>
<li>Driver 必须独立于 SUT，且不能随 SUT 变慢而同步降速；</li>
<li>延迟起点应尽可能接近事件产生，而不是方便测量的 Source 内部；</li>
<li>吞吐必须与 backlog、Event-time Latency 一起解释；</li>
<li>最大瞬时摄入速率不等于可长期维持的处理能力；</li>
<li>窗口、Join、倾斜与突发流量必须作为独立工作负载。</li>
</ol>
<h4 id="它没有解决什么-2">它没有解决什么</h4>
<p>首先，论文为了隔离引擎能力，主动移除了 Kafka、Redis 等外部系统。因此它回答的是“引擎在特定 Driver 下的能力”，不是完整生产链路的端到端容量。若性能声明包含消息队列和 Sink，它们就必须重新进入 SUT 边界。</p>
<p>其次，论文作者把以下问题留作后续工作：</p>
<ul>
<li>Exactly-once 的性能代价；</li>
<li>Out-of-order 与 Late Event；</li>
<li>故障、恢复与状态一致性；</li>
<li>更丰富的并发查询组合。</li>
</ul>
<p>最后，这是 2018 年特定版本与特定硬件上的结果，不能用来给今天的产品排名。我的进一步判断是：它对跨独立集群运行的不确定性讨论也弱于前两篇论文。因此，现代流系统 Benchmark 应把 2007/2013 年的独立重复、分层方差和效应量区间重新加到 2018 年的正确测量边界之上。</p>
<h3 id="14-三篇论文真正连起来的主线">1.4 三篇论文真正连起来的主线</h3>
<p>三篇论文分别强调统计、实验预算和系统语义，但最终可以收敛成四个连续问题：</p>
<table>
	<thead>
			<tr>
					<th>顺序</th>
					<th>必须回答的问题</th>
					<th>如果答错会怎样</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>1</td>
					<td>我想证明什么：冷启动、稳定态、单方法开销、端到端延迟，还是可持续吞吐？</td>
					<td>用正确工具回答错误问题</td>
			</tr>
			<tr>
					<td>2</td>
					<td>哪个才是独立实验单位：调用、Iteration、Fork、Build，还是完整 Cluster Run？</td>
					<td>伪造巨大样本量，低估不确定性</td>
			</tr>
			<tr>
					<td>3</td>
					<td>主要随机性和系统偏差来自哪里？</td>
					<td>在错误层级重复，或把时间漂移当代码差异</td>
			</tr>
			<tr>
					<td>4</td>
					<td>差异有多大、区间多宽、是否达到工程意义？</td>
					<td>把“统计上可区分”误写成“工程上值得”</td>
			</tr>
	</tbody>
</table>
<p>第一篇论文解决了第 2、4 个问题的基础；第二篇把第 2、3、4 个问题做成有限预算下的方法；第三篇则提醒我们，第 1 个问题一旦错了，后面的严谨统计没有意义。</p>
<h2 id="二jmh-怎样实现以及四个典型使用场景">二、JMH 怎样实现，以及四个典型使用场景</h2>
<p>JMH 是 OpenJDK 提供的 JVM Benchmark Harness。它最擅长测量单 JVM 内的 nano、micro、milli 级代码路径，也可以承载较大的方法级测试。</p>
<p>它能帮助解决：</p>
<ul>
<li>方法调用循环与计时；</li>
<li>JVM Fork、Warmup、Measurement；</li>
<li>多线程协同与 State 生命周期；</li>
<li>死代码消除、返回值消费；</li>
<li>参数组合与结果输出。</li>
</ul>
<p>它不能自动解决：</p>
<ul>
<li>被测代码是否代表真实业务；</li>
<li>Warmup 是否真的达到稳定且独立的状态；</li>
<li>多个 Iteration 是否可以当作独立实验；</li>
<li>baseline 与 candidate 是否受到机器漂移和执行顺序影响；</li>
<li>微基准改善能否转化成端到端吞吐或延迟改善。</li>
</ul>
<p>因此，JMH 是严谨实验的执行工具，不是结论生成器。</p>
<h3 id="21-jmh-不是用反射调用一次方法而是生成专用-harness">2.1 JMH 不是用反射调用一次方法，而是生成专用 Harness</h3>
<p>JMH 的关键实现位于官方仓库的几个部分：</p>
<ul>
<li><code>jmh-generator-annprocess</code> 中的 <code>BenchmarkProcessor</code> 是注解处理器；</li>
<li><code>BenchmarkGenerator</code> 根据 <code>@Benchmark</code>、<code>@State</code>、<code>@Setup</code> 等生成测试代码；</li>
<li><code>Runner</code> 解析配置并启动 Fork；</li>
<li><code>ForkedMain</code> 是被 Fork JVM 的入口；</li>
<li><code>ThroughputResult</code>、<code>AverageTimeResult</code> 等将操作数和时间转换成 Score。</li>
</ul>
<p>整体流程如下：</p>
<div class="mermaid">flowchart TB
    A["编写 @Benchmark 与 @State"] --> B["编译期注解处理器"]
    B --> C["生成专用 Harness<br/>以及 BenchmarkList"]
    C --> D["Host Runner 解析参数"]
    D --> E["启动独立 Forked JVM"]
    E --> F["执行 Warmup Iterations"]
    F --> G["执行 Measurement Iterations"]
    G --> H["聚合线程与 Iteration 结果<br/>输出文本或 JSON"]
</div>
<p>生成代码的核心逻辑可以简化为：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-java" data-lang="java"><span style="display:flex;"><span><span style="color:#66d9ef">while</span> (control.<span style="color:#a6e22e">warmupShouldWait</span>) {
</span></span><span style="display:flex;"><span>    benchmark(state);
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">long</span> start <span style="color:#f92672">=</span> System.<span style="color:#a6e22e">nanoTime</span>();
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">long</span> operations <span style="color:#f92672">=</span> 0;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">do</span> {
</span></span><span style="display:flex;"><span>    blackhole.<span style="color:#a6e22e">consume</span>(benchmark(state));
</span></span><span style="display:flex;"><span>    operations<span style="color:#f92672">++</span>;
</span></span><span style="display:flex;"><span>} <span style="color:#66d9ef">while</span> (<span style="color:#f92672">!</span>control.<span style="color:#a6e22e">isDone</span>);
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">long</span> elapsed <span style="color:#f92672">=</span> System.<span style="color:#a6e22e">nanoTime</span>() <span style="color:#f92672">-</span> start;
</span></span></code></pre></div><p>真实生成代码还会处理：</p>
<ul>
<li>多线程开始与结束屏障；</li>
<li><code>@Setup</code>、<code>@TearDown</code> 的 Trial、Iteration、Invocation 生命周期；</li>
<li><code>@OperationsPerInvocation</code> 与 Batch Size；</li>
<li>异常传播；</li>
<li>AuxCounters；</li>
<li>不同 Benchmark Mode 的专用循环。</li>
</ul>
<p>这种编译期生成有两个价值。</p>
<p>第一，测试方法位于紧凑循环内，不需要每次通过反射调用。第二，JMH 可以根据声明提前生成 State 获取、返回值消费和生命周期代码，并在编译期发现一部分非法组合。</p>
<p>如果 <code>@Benchmark</code> 返回非 <code>void</code> 值，生成代码会把返回值交给 <code>Blackhole.consume(...)</code>。所以最简单的防止死代码消除方式通常不是在业务代码里手写 Blackhole，而是直接返回计算结果。</p>
<h3 id="22-forkwarmupmeasurement-分别代表什么">2.2 Fork、Warmup、Measurement 分别代表什么</h3>
<p>一次典型 JMH 运行可以这样理解：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>一个 Benchmark 参数组合
</span></span><span style="display:flex;"><span>  ├─ Fork 1：全新的 JVM
</span></span><span style="display:flex;"><span>  │    ├─ Warmup Iteration 1..W
</span></span><span style="display:flex;"><span>  │    └─ Measurement Iteration 1..M
</span></span><span style="display:flex;"><span>  ├─ Fork 2：全新的 JVM
</span></span><span style="display:flex;"><span>  │    ├─ Warmup Iteration 1..W
</span></span><span style="display:flex;"><span>  │    └─ Measurement Iteration 1..M
</span></span><span style="display:flex;"><span>  └─ Fork F：...
</span></span></code></pre></div><ul>
<li>Fork：新的 JVM 进程，拥有新的 JIT Profile、堆、地址空间和运行时历史；</li>
<li>Warmup Iteration：执行负载但不进入最终 Score，用于把 JVM 推到目标生命周期位置；</li>
<li>Measurement Iteration：在固定时间窗口内反复调用 Benchmark 方法并形成一个 Iteration Score；</li>
<li>Invocation：Benchmark 方法的一次调用，通常数量极大，但不是独立 JVM 实验。</li>
</ul>
<p>JMH 的四种常用 Mode 回答不同问题：</p>
<table>
	<thead>
			<tr>
					<th>Mode</th>
					<th>Score 含义</th>
					<th>适合场景</th>
					<th>主要风险</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Throughput</td>
					<td>单位时间完成的操作数</td>
					<td>队列、计数器、解析器的处理能力</td>
					<td>高吞吐不代表低尾延迟</td>
			</tr>
			<tr>
					<td>AverageTime</td>
					<td>每次操作的平均时间</td>
					<td>稳定热路径的平均成本</td>
					<td>平均值会隐藏长尾</td>
			</tr>
			<tr>
					<td>SampleTime</td>
					<td>对部分调用采样并形成分布</td>
					<td>观察方法级延迟分布</td>
					<td>仍不是外部固定到达率下的系统延迟</td>
			</tr>
			<tr>
					<td>SingleShotTime</td>
					<td>单次调用或一个 Batch 的时间</td>
					<td>冷启动、一次性初始化、较长操作</td>
					<td>必须通过多个 Fork 获得独立重复</td>
			</tr>
	</tbody>
</table>
<p>Throughput 与 AverageTime 的核心换算非常简单：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Throughput  = N / T
</span></span><span style="display:flex;"><span>AverageTime = T / N
</span></span></code></pre></div><p>其中：</p>
<ul>
<li><code>N</code> 是 Measurement 时间内完成的操作数；</li>
<li><code>T</code> 是 Measurement 的有效计时时间；</li>
<li>多线程 Throughput 会先聚合线程结果；</li>
<li><code>@OperationsPerInvocation</code> 会改变一次 Benchmark invocation 代表的逻辑操作数。</li>
</ul>
<p>不要只因为 <code>ns/op</code> 看起来直观，就默认选择 AverageTime；Mode 必须服从问题。如果想知道 8 个线程争用一个计数器时每秒能完成多少次更新，Throughput 更自然。如果想测一次冷初始化，应该考虑 <code>SingleShotTime</code>、关闭 Warmup，并增加独立 Fork。</p>
<h3 id="23-建立项目运行与读懂输出">2.3 建立项目、运行与读懂输出</h3>
<p>JMH 官方推荐使用独立 Maven Benchmark 模块，而不是像 JUnit 一样只添加一个依赖后直接从 IDE 运行。下面使用当前稳定版 1.37 创建项目：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>mvn archetype:generate <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -DinteractiveMode<span style="color:#f92672">=</span>false <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -DarchetypeGroupId<span style="color:#f92672">=</span>org.openjdk.jmh <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -DarchetypeArtifactId<span style="color:#f92672">=</span>jmh-java-benchmark-archetype <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -DarchetypeVersion<span style="color:#f92672">=</span>1.37 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -DgroupId<span style="color:#f92672">=</span>org.example <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -DartifactId<span style="color:#f92672">=</span>benchmark <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -Dversion<span style="color:#f92672">=</span>1.0
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>cd benchmark
</span></span><span style="display:flex;"><span>mvn clean verify
</span></span><span style="display:flex;"><span>java -jar target/benchmarks.jar -l
</span></span></code></pre></div><p>一个适合首次定标的命令是：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>java -jar target/benchmarks.jar <span style="color:#e6db74">&#39;.*HashBenchmark.*&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -f <span style="color:#ae81ff">5</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -wi <span style="color:#ae81ff">5</span> -w 1s <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -i <span style="color:#ae81ff">10</span> -r 1s <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -rf json <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -rff target/jmh-result.json
</span></span></code></pre></div><p>参数含义如下：</p>
<table>
	<thead>
			<tr>
					<th>参数</th>
					<th>含义</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>-f 5</code></td>
					<td>运行 5 个独立 Fork</td>
			</tr>
			<tr>
					<td><code>-wi 5</code></td>
					<td>每个 Fork 运行 5 个 Warmup Iteration</td>
			</tr>
			<tr>
					<td><code>-w 1s</code></td>
					<td>每个 Warmup Iteration 运行 1 秒</td>
			</tr>
			<tr>
					<td><code>-i 10</code></td>
					<td>每个 Fork 运行 10 个 Measurement Iteration</td>
			</tr>
			<tr>
					<td><code>-r 1s</code></td>
					<td>每个 Measurement Iteration 运行 1 秒</td>
			</tr>
			<tr>
					<td><code>-rf json</code></td>
					<td>输出机器可读 JSON</td>
			</tr>
			<tr>
					<td><code>-rff ...</code></td>
					<td>指定结果文件</td>
			</tr>
	</tbody>
</table>
<p>这些数字只是第一次观察时间序列和方差的起点，不是三篇论文推导出的通用最优值。正式比较前，应根据 Benchmark、JDK、机器与目标精度调整。</p>
<p>典型输出类似：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Benchmark          Mode  Cnt   Score   Error  Units
</span></span><span style="display:flex;"><span>HashBenchmark.mix  avgt   50  37.210 ± 0.880  ns/op
</span></span></code></pre></div><p>这是一行教学输出，不是本文实测结论：</p>
<ul>
<li><code>Mode=avgt</code> 表示 AverageTime；</li>
<li><code>Cnt=50</code> 通常来自 5 Fork × 10 Measurement Iteration，不是只调用了 50 次；</li>
<li><code>Score=37.210 ns/op</code> 是聚合后的平均操作时间；</li>
<li><code>Error=0.880</code> 是 JMH 展示的 99.9% 均值误差范围；</li>
<li>JMH 的扩展输出会明确写出该区间假设正态分布。</li>
</ul>
<p>这里必须保持克制：JMH 展示的 <code>Score ± Error</code> 不是 baseline 与 candidate 的效应量区间，也不会证明 50 个 Iteration 全部独立。做版本回归判断时，应保存 JSON 原始结果，查看每个 Fork 的分布，并计算 candidate/baseline 的变化幅度与区间。</p>
<h3 id="24-场景一纯计算防止死代码消除与常量折叠">2.4 场景一：纯计算——防止死代码消除与常量折叠</h3>
<p>下面测量一个整数混合函数：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-java" data-lang="java"><span style="display:flex;"><span><span style="color:#f92672">package</span> org.example;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> java.util.concurrent.TimeUnit;
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> org.openjdk.jmh.annotations.Benchmark;
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> org.openjdk.jmh.annotations.BenchmarkMode;
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> org.openjdk.jmh.annotations.Mode;
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> org.openjdk.jmh.annotations.OutputTimeUnit;
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> org.openjdk.jmh.annotations.Param;
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> org.openjdk.jmh.annotations.Scope;
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> org.openjdk.jmh.annotations.State;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@State</span>(Scope.<span style="color:#a6e22e">Thread</span>)
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@BenchmarkMode</span>(Mode.<span style="color:#a6e22e">AverageTime</span>)
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@OutputTimeUnit</span>(TimeUnit.<span style="color:#a6e22e">NANOSECONDS</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">HashBenchmark</span> {
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@Param</span>({<span style="color:#e6db74">&#34;7&#34;</span>, <span style="color:#e6db74">&#34;42&#34;</span>})
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">int</span> seed;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@Param</span>({<span style="color:#e6db74">&#34;64&#34;</span>, <span style="color:#e6db74">&#34;1024&#34;</span>})
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">int</span> rounds;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@Benchmark</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">int</span> <span style="color:#a6e22e">mix</span>() {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">int</span> value <span style="color:#f92672">=</span> seed;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> (<span style="color:#66d9ef">int</span> i <span style="color:#f92672">=</span> 0; i <span style="color:#f92672">&lt;</span> rounds; i<span style="color:#f92672">++</span>) {
</span></span><span style="display:flex;"><span>            value <span style="color:#f92672">=</span> Integer.<span style="color:#a6e22e">rotateLeft</span>(value <span style="color:#f92672">*</span> 31 <span style="color:#f92672">+</span> i, 5);
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> value;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>这里有三个关键点：</p>
<ol>
<li><code>seed</code> 和 <code>rounds</code> 来自非 final 的 State 字段，避免把整个输入写成编译期常量；</li>
<li>方法返回结果，JMH 生成代码会消费它，降低整个计算被当作无用代码删除的风险；</li>
<li><code>Scope.Thread</code> 让每个线程拥有自己的 State，避免无意中测到共享字段争用。</li>
</ol>
<p>下面这种写法是危险的：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-java" data-lang="java"><span style="display:flex;"><span><span style="color:#a6e22e">@Benchmark</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> <span style="color:#a6e22e">wrong</span>() {
</span></span><span style="display:flex;"><span>    mixConstant(42, 1024);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>输入固定、结果又没有被使用，JIT 可能进行常量折叠或死代码消除。最后测到的可能只是空 Harness 的成本。</p>
<p>这个场景适合纯算法、编码、哈希、序列化内部函数等 CPU 热路径。但如果生产输入有不同长度、字符集或分支分布，就必须用 <code>@Param</code> 或预生成数据覆盖这些形态，不能只测最容易优化的一种输入。</p>
<h3 id="25-场景二数据结构用-state-与生命周期隔离准备成本">2.5 场景二：数据结构——用 State 与生命周期隔离准备成本</h3>
<p>下面比较有序数组上的线性查找与二分查找：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-java" data-lang="java"><span style="display:flex;"><span><span style="color:#f92672">package</span> org.example;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> java.util.Arrays;
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> java.util.concurrent.TimeUnit;
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> org.openjdk.jmh.annotations.Benchmark;
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> org.openjdk.jmh.annotations.BenchmarkMode;
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> org.openjdk.jmh.annotations.Level;
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> org.openjdk.jmh.annotations.Mode;
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> org.openjdk.jmh.annotations.OutputTimeUnit;
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> org.openjdk.jmh.annotations.Param;
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> org.openjdk.jmh.annotations.Scope;
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> org.openjdk.jmh.annotations.Setup;
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> org.openjdk.jmh.annotations.State;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@State</span>(Scope.<span style="color:#a6e22e">Thread</span>)
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@BenchmarkMode</span>(Mode.<span style="color:#a6e22e">AverageTime</span>)
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@OutputTimeUnit</span>(TimeUnit.<span style="color:#a6e22e">NANOSECONDS</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">LookupBenchmark</span> {
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@Param</span>({<span style="color:#e6db74">&#34;16&#34;</span>, <span style="color:#e6db74">&#34;1024&#34;</span>, <span style="color:#e6db74">&#34;65536&#34;</span>})
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">int</span> size;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">int</span><span style="color:#f92672">[]</span> values;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">int</span> key;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@Setup</span>(Level.<span style="color:#a6e22e">Trial</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> <span style="color:#a6e22e">prepareDataset</span>() {
</span></span><span style="display:flex;"><span>        values <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#66d9ef">int</span><span style="color:#f92672">[</span>size<span style="color:#f92672">]</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> (<span style="color:#66d9ef">int</span> i <span style="color:#f92672">=</span> 0; i <span style="color:#f92672">&lt;</span> size; i<span style="color:#f92672">++</span>) {
</span></span><span style="display:flex;"><span>            values<span style="color:#f92672">[</span>i<span style="color:#f92672">]</span> <span style="color:#f92672">=</span> i <span style="color:#f92672">*</span> 2;
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        key <span style="color:#f92672">=</span> values<span style="color:#f92672">[</span>size <span style="color:#f92672">-</span> 1<span style="color:#f92672">]</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@Benchmark</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">int</span> <span style="color:#a6e22e">linearSearch</span>() {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> (<span style="color:#66d9ef">int</span> i <span style="color:#f92672">=</span> 0; i <span style="color:#f92672">&lt;</span> values.<span style="color:#a6e22e">length</span>; i<span style="color:#f92672">++</span>) {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> (values<span style="color:#f92672">[</span>i<span style="color:#f92672">]</span> <span style="color:#f92672">==</span> key) {
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">return</span> i;
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#f92672">-</span>1;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@Benchmark</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">int</span> <span style="color:#a6e22e">binarySearch</span>() {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> Arrays.<span style="color:#a6e22e">binarySearch</span>(values, key);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><code>@Setup(Level.Trial)</code> 在一个 Benchmark Trial 开始时准备一次数组，数组构造不进入查询的计时结果。这样测到的是“已有数据结构上的查找成本”。</p>
<p>如果真正的问题是“构造并查询一次需要多久”，那就应该另写一个 Benchmark，把构造放进 <code>@Benchmark</code>。不要为了得到更小的数字，把生产路径必需的工作偷偷移到 Setup。</p>
<p>三个常见生命周期分别是：</p>
<table>
	<thead>
			<tr>
					<th>Level</th>
					<th>调用时机</th>
					<th>适合内容</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Trial</td>
					<td>每个完整 Benchmark Trial 一次</td>
					<td>大数据集、连接、共享结构的初始化</td>
			</tr>
			<tr>
					<td>Iteration</td>
					<td>每个 Warmup/Measurement Iteration 一次</td>
					<td>每个时间窗口要重置的计数或批次状态</td>
			</tr>
			<tr>
					<td>Invocation</td>
					<td>每次 Benchmark 调用前后</td>
					<td>必须逐操作恢复的状态，谨慎使用</td>
			</tr>
	</tbody>
</table>
<p><code>Level.Invocation</code> 的 Setup 时间可以从有效计时中扣除，但它仍会改变缓存、分支预测和对象状态，而且每次调用都有 Harness 成本。除非被测语义确实要求逐次重置，否则优先批量准备输入或使用多个预生成样本。</p>
<h3 id="26-场景三并发争用scope-选错测量问题就变了">2.6 场景三：并发争用——Scope 选错，测量问题就变了</h3>
<p>下面比较共享 <code>AtomicLong</code> 与 <code>LongAdder</code> 的更新吞吐：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-java" data-lang="java"><span style="display:flex;"><span><span style="color:#f92672">package</span> org.example;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> java.util.concurrent.TimeUnit;
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> java.util.concurrent.atomic.AtomicLong;
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> java.util.concurrent.atomic.LongAdder;
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> org.openjdk.jmh.annotations.Benchmark;
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> org.openjdk.jmh.annotations.BenchmarkMode;
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> org.openjdk.jmh.annotations.Mode;
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> org.openjdk.jmh.annotations.OutputTimeUnit;
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> org.openjdk.jmh.annotations.Scope;
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> org.openjdk.jmh.annotations.State;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@State</span>(Scope.<span style="color:#a6e22e">Benchmark</span>)
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@BenchmarkMode</span>(Mode.<span style="color:#a6e22e">Throughput</span>)
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@OutputTimeUnit</span>(TimeUnit.<span style="color:#a6e22e">SECONDS</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">ContendedCounterBenchmark</span> {
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">final</span> AtomicLong atomic <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> AtomicLong();
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">final</span> LongAdder adder <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> LongAdder();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@Benchmark</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> <span style="color:#a6e22e">atomicIncrement</span>() {
</span></span><span style="display:flex;"><span>        atomic.<span style="color:#a6e22e">incrementAndGet</span>();
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@Benchmark</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> <span style="color:#a6e22e">adderIncrement</span>() {
</span></span><span style="display:flex;"><span>        adder.<span style="color:#a6e22e">increment</span>();
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>分别运行单线程与多线程：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>java -jar target/benchmarks.jar ContendedCounterBenchmark -t <span style="color:#ae81ff">1</span> -f <span style="color:#ae81ff">5</span>
</span></span><span style="display:flex;"><span>java -jar target/benchmarks.jar ContendedCounterBenchmark -t <span style="color:#ae81ff">8</span> -f <span style="color:#ae81ff">5</span>
</span></span></code></pre></div><p><code>Scope.Benchmark</code> 使同一个 Trial 中的所有工作线程共享一个 State，因此多线程运行确实会争用同一计数器。如果改成 <code>Scope.Thread</code>，8 个线程会更新 8 份独立 State，测到的是并行独享吞吐，而不是共享争用。</p>
<p>这个例子还有一个比性能更重要的语义提醒：</p>
<ul>
<li><code>AtomicLong.incrementAndGet()</code> 可以为每次更新提供一个线性化后的返回值；</li>
<li><code>LongAdder</code> 通过分散竞争提高高并发更新能力，但读取总和不是同一种强一致语义。</li>
</ul>
<p>因此，不能根据 LongAdder 的 ops/s 更高就宣布它“全面替代” AtomicLong。Benchmark 只有在候选实现满足同一业务契约时，才有比较意义。</p>
<h3 id="27-场景四快慢路径用-auxcounters-解释主-score">2.7 场景四：快慢路径——用 AuxCounters 解释主 Score</h3>
<p>平均吞吐可能掩盖输入分布变化。下面让 0%、1% 或 10% 的输入触发异常解析路径，并用 AuxCounters 同时报告成功与失败速率：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-java" data-lang="java"><span style="display:flex;"><span><span style="color:#f92672">package</span> org.example;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> java.util.concurrent.TimeUnit;
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> org.openjdk.jmh.annotations.AuxCounters;
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> org.openjdk.jmh.annotations.Benchmark;
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> org.openjdk.jmh.annotations.BenchmarkMode;
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> org.openjdk.jmh.annotations.Level;
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> org.openjdk.jmh.annotations.Mode;
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> org.openjdk.jmh.annotations.OutputTimeUnit;
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> org.openjdk.jmh.annotations.Param;
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> org.openjdk.jmh.annotations.Scope;
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> org.openjdk.jmh.annotations.Setup;
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> org.openjdk.jmh.annotations.State;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@BenchmarkMode</span>(Mode.<span style="color:#a6e22e">Throughput</span>)
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@OutputTimeUnit</span>(TimeUnit.<span style="color:#a6e22e">SECONDS</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">ParserBenchmark</span> {
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@State</span>(Scope.<span style="color:#a6e22e">Thread</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Inputs</span> {
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">@Param</span>({<span style="color:#e6db74">&#34;0&#34;</span>, <span style="color:#e6db74">&#34;1&#34;</span>, <span style="color:#e6db74">&#34;10&#34;</span>})
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">int</span> invalidPercent;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">private</span> String<span style="color:#f92672">[]</span> values;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">int</span> cursor;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">@Setup</span>(Level.<span style="color:#a6e22e">Trial</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> <span style="color:#a6e22e">prepare</span>() {
</span></span><span style="display:flex;"><span>            values <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> String<span style="color:#f92672">[</span>1024<span style="color:#f92672">]</span>;
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">for</span> (<span style="color:#66d9ef">int</span> i <span style="color:#f92672">=</span> 0; i <span style="color:#f92672">&lt;</span> values.<span style="color:#a6e22e">length</span>; i<span style="color:#f92672">++</span>) {
</span></span><span style="display:flex;"><span>                values<span style="color:#f92672">[</span>i<span style="color:#f92672">]</span> <span style="color:#f92672">=</span> i <span style="color:#f92672">%</span> 100 <span style="color:#f92672">&lt;</span> invalidPercent
</span></span><span style="display:flex;"><span>                        <span style="color:#f92672">?</span> <span style="color:#e6db74">&#34;invalid&#34;</span>
</span></span><span style="display:flex;"><span>                        : Integer.<span style="color:#a6e22e">toString</span>(i);
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> String <span style="color:#a6e22e">next</span>() {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> values<span style="color:#f92672">[</span>cursor<span style="color:#f92672">++</span> <span style="color:#f92672">&amp;</span> (values.<span style="color:#a6e22e">length</span> <span style="color:#f92672">-</span> 1)<span style="color:#f92672">]</span>;
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@State</span>(Scope.<span style="color:#a6e22e">Thread</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@AuxCounters</span>(AuxCounters.<span style="color:#a6e22e">Type</span>.<span style="color:#a6e22e">OPERATIONS</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Outcomes</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">long</span> valid;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">long</span> invalid;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@Benchmark</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">int</span> <span style="color:#a6e22e">parse</span>(Inputs inputs, Outcomes outcomes) {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">try</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">int</span> value <span style="color:#f92672">=</span> Integer.<span style="color:#a6e22e">parseInt</span>(inputs.<span style="color:#a6e22e">next</span>());
</span></span><span style="display:flex;"><span>            outcomes.<span style="color:#a6e22e">valid</span><span style="color:#f92672">++</span>;
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> value;
</span></span><span style="display:flex;"><span>        } <span style="color:#66d9ef">catch</span> (NumberFormatException e) {
</span></span><span style="display:flex;"><span>            outcomes.<span style="color:#a6e22e">invalid</span><span style="color:#f92672">++</span>;
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#f92672">-</span>1;
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>运行后，除了主 Benchmark 的总吞吐，还会看到 <code>valid</code> 与 <code>invalid</code> 两个 Secondary Result。这样可以回答：</p>
<ul>
<li>总吞吐下降是不是因为失败比例提高；</li>
<li>慢路径究竟占多少操作；</li>
<li>两个版本是否处理了相同输入分布。</li>
</ul>
<p><code>@AuxCounters</code> 的边界必须讲清楚：</p>
<ul>
<li>它只能标注 <code>Scope.Thread</code> 的 State；</li>
<li>只有 public 数值字段或返回数值的 public 方法会成为指标；</li>
<li>public 字段会在每个 Iteration 开始前重置，并在结束时读取；</li>
<li><code>Type.OPERATIONS</code> 会按时间归一化，适合成功/失败吞吐；</li>
<li><code>Type.EVENTS</code> 报告未按时间归一化的事件总数；</li>
<li>它是实验性 API，适合解释分支构成，不应代替主性能指标与正确性断言。</li>
</ul>
<h3 id="28-怎样从-jmh-输出得到版本结论">2.8 怎样从 JMH 输出得到版本结论</h3>
<p>假设 AverageTime 模式下定义：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>R = candidate time / baseline time
</span></span></code></pre></div><p>其中：</p>
<ul>
<li><code>R &lt; 1</code> 表示 candidate 更快；</li>
<li><code>R &gt; 1</code> 表示 candidate 更慢；</li>
<li><code>R = 1</code> 表示两者平均时间相同。</li>
</ul>
<p>正式比较至少应做到：</p>
<ol>
<li>使用相同硬件、JDK、JVM 参数和 Benchmark 参数；</li>
<li>运行多个 Fork，保留每个 Fork/Iteration 的 JSON 结果；</li>
<li>交替或随机化 baseline 与 candidate 的执行顺序，避免时间漂移总偏向一方；</li>
<li>报告 <code>R</code> 的区间，而不是只比较两行 Score；</li>
<li>预先定义工程阈值，例如小于 1% 的变化不做优化结论；</li>
<li>同时检查正确性、分配率、GC 与其他可能被转移的成本。</li>
</ol>
<p>以“1% 是最小关注变化”为例：</p>
<table>
	<thead>
			<tr>
					<th>比值区间</th>
					<th>合理结论</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>整个区间低于 0.99</td>
					<td>有证据表明 candidate 达到关注阈值以上的改善</td>
			</tr>
			<tr>
					<td>整个区间高于 1.01</td>
					<td>有证据表明出现达到关注阈值以上的回归</td>
			</tr>
			<tr>
					<td>整个区间位于 0.99～1.01</td>
					<td>在当前精度与阈值下可认为工程上近似等价</td>
			</tr>
			<tr>
					<td>区间跨越这些边界</td>
					<td>证据不足，应增加独立重复或检查噪声来源</td>
			</tr>
	</tbody>
</table>
<p>如果使用 Throughput，方向要反过来定义，或者改为 <code>baseline throughput / candidate throughput</code>，让“小于 1 表示改善”的语义保持一致。无论怎样定义，都必须在报告中写清分子、分母和“越大越好还是越小越好”。</p>
<p>JMH 的 profiler 可以帮助解释结果：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>java -jar target/benchmarks.jar LookupBenchmark -prof gc
</span></span></code></pre></div><p>例如一个实现 ns/op 下降，却显著增加 <code>gc.alloc.rate.norm</code>，它可能只是把 CPU 成本换成了分配和 GC 成本。Profiler 是归因线索，不是自动因果证明；发现异常后仍需要结合代码、汇编、硬件计数器或更高层测试验证。</p>
<h2 id="三总结与感悟">三、总结与感悟</h2>
<p>读完三篇论文再回看 JMH，我最大的感受是：性能测试的难点从来不是让程序多执行几遍，而是知道每一个数字究竟代表什么。</p>
<p>第一篇论文最重要的提醒是，不要把偶然的最好值当作能力，也不要把同一 JVM 中的大量调用当作大量独立证据。Java 的动态运行时决定了 Fork 层级不可忽略。</p>
<p>第二篇论文进一步说明，严谨并不等于无限增加运行时间。真正有效的方法是先找出方差来自 Build、Fork 还是 Iteration，再把重复预算花到正确层级。跑得更多不一定知道得更多；在错误层级重复，只会更精确地低估不确定性。</p>
<p>第三篇论文把视线从统计拉回系统语义。一个指标可以测得非常稳定，却从错误的时间点开始计时。流处理系统尤其如此：如果没有固定到达率、外部时间戳、backlog 与 Event-time Latency，背压完全可能把容量不足伪装成低延迟。</p>
<p>JMH 的价值，是把 JVM 内部微基准中最容易犯的一批 Harness 错误标准化处理：生成专用循环、启动 Fork、组织 Warmup 与 Measurement、管理 State、消费返回值、输出机器可读结果。但它无法替我们决定测量边界，也不会自动证明 Warmup 已经稳定，更不会把一个 5 ns 的方法级改善自动转换成系统吞吐提升。</p>
<p>因此，我现在更愿意用下面这句话定义一份好的 Benchmark：</p>
<blockquote>
<p>它不是给出一个漂亮数字，而是让别人能够明确知道这个数字在什么条件下成立、误差有多大、什么证据可以推翻它。</p>
</blockquote>
<p>真正开始写 Benchmark 之前，可以先写出性能声明：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>在固定的硬件、JDK、参数和输入分布下，
</span></span><span style="display:flex;"><span>candidate 相对 baseline 改善哪一个指标，
</span></span><span style="display:flex;"><span>改善幅度至少是多少，
</span></span><span style="display:flex;"><span>并且没有破坏哪一种正确性与资源约束。
</span></span></code></pre></div><p>如果这句话写不清楚，代码暂时也不用急着写。因为 Benchmark 的顺序应该是：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>先定义问题 → 再设计实验 → 然后运行工具 → 最后解释证据
</span></span></code></pre></div><p>而不是先得到一个数字，再回头为它寻找故事。</p>
<h3 id="参考资料">参考资料</h3>
<ol>
<li>Andy Georges, Dries Buytaert, Lieven Eeckhout, <a href="https://dri.es/files/oopsla07-georges.pdf">Statistically Rigorous Java Performance Evaluation</a>, OOPSLA 2007.</li>
<li>Tomas Kalibera, Richard Jones, <a href="https://dl.acm.org/doi/10.1145/2491894.2464160">Rigorous Benchmarking in Reasonable Time</a>, ISMM 2013；<a href="https://www.researchgate.net/publication/257193308_Rigorous_Benchmarking_in_Reasonable_Time">可访问全文</a>.</li>
<li>Jeyhun Karimov et al., <a href="https://arxiv.org/pdf/1802.08496">Benchmarking Distributed Stream Data Processing Systems</a>, ICDE 2018.</li>
<li>OpenJDK, <a href="https://github.com/openjdk/jmh">Java Microbenchmark Harness</a>.</li>
<li>OpenJDK JMH, <a href="https://github.com/openjdk/jmh/tree/master/jmh-samples/src/main/java/org/openjdk/jmh/samples">官方示例目录</a>.</li>
<li>OpenJDK JMH 源码：<a href="https://github.com/openjdk/jmh/blob/master/jmh-generator-annprocess/src/main/java/org/openjdk/jmh/generators/BenchmarkProcessor.java">BenchmarkProcessor</a>、<a href="https://github.com/openjdk/jmh/blob/master/jmh-core/src/main/java/org/openjdk/jmh/generators/core/BenchmarkGenerator.java">BenchmarkGenerator</a>、<a href="https://github.com/openjdk/jmh/blob/master/jmh-core/src/main/java/org/openjdk/jmh/runner/Runner.java">Runner</a>、<a href="https://github.com/openjdk/jmh/blob/master/jmh-core/src/main/java/org/openjdk/jmh/results/Result.java">Result</a>.</li>
</ol>
]]></content:encoded></item></channel></rss>