IsolatedTaskScheduler 性能基准报告
对应基准代码:
benchmarks/TC.Tier.Core.Benchmarks/Execution/IsolatedTaskSchedulerBench.cs使用指南(选型/旋钮/注意事项):../dedicated-task-scheduler.md⚠️ 本文数字为指示值(单机开发环境实测,BenchmarkDotNet.Artifacts 不入库,本文是结果的存档); 机器 / 负载 / .NET 版本不同会变,以 §1 命令在本机复跑为准。
1. 复跑命令
# 完整跑(Release 必须)
dotnet run -c Release --project benchmarks/TC.Tier.Core.Benchmarks -- --filter *IsolatedTaskSchedulerBench*
# 快跑冒烟(粗数字)
dotnet run -c Release --project benchmarks/TC.Tier.Core.Benchmarks -- --filter *IsolatedTaskSchedulerBench* --job short
2. 环境
2026-08-17 | Windows 11(10.0.26200)| i5-12400(6C/12T)| .NET 8.0.30 | BenchmarkDotNet 0.13.12(warmup 3 / iteration 5)
3. 基准设计(测什么 / 为什么)
| 组 | 基准 | 回答的问题 |
|---|---|---|
| 1 | 单任务往返(StartNew no-op → 私有线程执行 → 完成) |
隔离的每次派发税多贵?与公共池(TaskScheduler.Default)对照 |
| 2 | 同上 + 指标开(noop sink) | 指标采集的热路径开销(默认 Disabled 零开销声明的另一面:开了多贵) |
| 3 | continuation 回流(任务内 await Task.Yield() ×100) |
每次 await 让出后经 QueueTask 回流私有线程的成本(协作 async 的核心路径) |
| 4 | 多生产者吞吐(4 公共池生产者 × 250 no-op 任务) | 突发灌入下调度器排空能力;默认有界队列(背压阻塞)vs 无界 vs 公共池 |
4. 结果
参数:YieldCount=100;M ∈ {1,2,4};每组以同 M 的公共池行为基线(三组基线实测一致:往返 ~650ns、yield 循环 ~33µs、吞吐 ~76µs)。
M=1
| Method | Mean | StdDev | Ratio | Gen0 | Allocated |
|---|---|---|---|---|---|
| ThreadPool round-trip | 648.0 ns | 15.41 ns | 1.00 | 0.0067 | 64 B |
| IsolatedTaskScheduler round-trip | 485.0 ns | 14.92 ns | 0.76 | 0.0067 | 64 B |
| Isolated round-trip, metrics ON | 1,195.2 ns | 15.65 ns | 1.85 | 0.0057 | 64 B |
| ThreadPool yield-loop ×100 | 33,248.3 ns | 159.59 ns | 51.81 | - | 416 B |
| IsolatedTaskScheduler yield-loop ×100 | 31,588.7 ns | 1,239.56 ns | 48.75 | 0.6714 | 6,878 B |
| ThreadPool throughput 4×250 | 76,296.1 ns | 520.93 ns | 117.80 | 7.9346 | 73,745 B |
| Isolated throughput 4×250(有界队列) | 716,505.0 ns | 28,562.30 ns | 1,106.15 | 15.6250 | 149,620 B |
| Isolated throughput 4×250(无界队列) | 295,469.5 ns | 4,008.87 ns | 456.10 | 7.8125 | 73,808 B |
M=2(推荐默认)
| Method | Mean | StdDev | Ratio | Gen0 | Allocated |
|---|---|---|---|---|---|
| ThreadPool round-trip | 646.4 ns | 5.05 ns | 1.00 | 0.0067 | 64 B |
| IsolatedTaskScheduler round-trip | 950.6 ns | 16.14 ns | 1.47 | 0.0057 | 64 B |
| Isolated round-trip, metrics ON | 1,072.5 ns | 10.05 ns | 1.66 | 0.0057 | 64 B |
| ThreadPool yield-loop ×100 | 32,596.9 ns | 268.19 ns | 50.43 | - | 416 B |
| IsolatedTaskScheduler yield-loop ×100 | 51,398.6 ns | 1,485.11 ns | 79.53 | 0.7324 | 6,879 B |
| ThreadPool throughput 4×250 | 75,616.8 ns | 358.35 ns | 116.95 | 7.9346 | 73,745 B |
| Isolated throughput 4×250(有界队列) | 585,702.6 ns | 8,135.82 ns | 905.93 | 11.7188 | 113,505 B |
| Isolated throughput 4×250(无界队列) | 361,025.8 ns | 2,425.74 ns | 558.58 | 7.8125 | 73,807 B |
M=4
| Method | Mean | StdDev | Ratio | Gen0 | Allocated |
|---|---|---|---|---|---|
| ThreadPool round-trip | 667.4 ns | 6.52 ns | 1.00 | 0.0067 | 64 B |
| IsolatedTaskScheduler round-trip | 995.2 ns | 33.72 ns | 1.49 | 0.0057 | 64 B |
| Isolated round-trip, metrics ON | 1,085.2 ns | 9.26 ns | 1.63 | 0.0057 | 64 B |
| ThreadPool yield-loop ×100 | 32,823.6 ns | 320.16 ns | 49.19 | - | 416 B |
| IsolatedTaskScheduler yield-loop ×100 | 53,065.0 ns | 1,200.94 ns | 79.51 | 0.7324 | 6,879 B |
| ThreadPool throughput 4×250 | 75,100.1 ns | 100.72 ns | 112.92 | 7.9346 | 73,745 B |
| Isolated throughput 4×250(有界队列) | 575,776.2 ns | 9,840.19 ns | 862.83 | 8.7891 | 89,905 B |
| Isolated throughput 4×250(无界队列) | 416,066.2 ns | 3,073.60 ns | 623.46 | 7.8125 | 73,752 B |
注:yield/吞吐组的 Ratio 是 BDN 以同组往返基线为 1.00 的换算(故公共池自身显示 50×/117×);直接比 Mean 即: yield M≥2 ≈ 池的 1.6×;吞吐有界 ≈ 池的 7.6~9.4×、无界 ≈ 3.9~5.5×。
5. 结论
- 派发税亚微秒:单任务往返 0.5~1µs,分配与公共池完全相同(64 B/任务 = Task 对象本身,调度零额外分配)。真实 worker 任务(µs~ms 级建段/IO)下派发税占比可忽略。M=1 的单消费者阻塞交接(~485ns)甚至快于公共池全局队列(~650ns)——单线程专用实例无排队竞争。
- continuation 回流 ~0.3µs/次(M=1)/ ~0.5µs/次(M≥2),每次多 ~64 B 分配(BlockingCollection 节点):await 密集的真实 worker 摊薄无感;纯
Task.Yield空转微循环会看到 1.6×(M≥2)——这是调度器的病理用法,见使用指南 §1 不适用表。M=1 ≈ 池速。 - no-op 吞吐天花板 ~2.4-3.4M 任务/s(M=1..4,无界队列)vs 公共池(12 逻辑核)~13M/s——M 个私有线程换确定性/隔离是有意限流,不是缺陷;需要 no-op 火力全开的场景本来就不该用它。
- 默认有界队列(cap=max(M*4,16)≈16-20)在 4 生产者突发下触发阻塞背压:比无界慢 1.5~2×(M=1: 717µs vs 295µs),且 Gen0 压力翻倍(生产者线程在
SemaphoreSlim上阻塞/唤醒的额外分配,M=1 时 Allocated 149.6KB vs 73.8KB)。 - 指标开仅 +0.1~0.7µs/任务、零额外分配(64 B 与关时相同)——生产环境可常开,不必为省这点关掉可观测性。
6. 调参指引(由数据推出)
| 症状 | 数据依据 | 动作 |
|---|---|---|
scheduler.queue.full 频发、生产者延迟敏感 |
§4 结论 4:有界背压在突发下慢 1.5~2× + GC 翻倍 | 确认 worker 工作项队列才是背压主战场(使用指南 §7.3)后,调大 QueueCapacity(如 M*16+)或 -1 无界(配合 queue.depth 监控) |
| await 密集 worker 回流延迟敏感 | §4 结论 2:M=1 ≈ 池速、M≥2 1.6× | 该 worker own 一个 M=1 实例(回流路径单消费者最快);并行度靠多实例或 worker 队列 |
| 需要 no-op 级超大吞吐 | §4 结论 3:天花板 ~M×0.85M 任务/s | 这不是本组件的场景——用公共池或独立分区;或加大 M(受核数校验约束) |
| 想省指标开销 | §4 结论 5:+0.1~0.7µs/任务 | 通常不值得关;确需极限,默认 Hub=null(Disabled)即零开销(IsEnabled 一次读短路) |