微基准测试中Async ValueTask为何比Async Task性能更低?
为什么等待ValueTask的开销是Task的4倍?
我做了一个微基准测试,发现等待ValueTask方法的开销大约是等待Task方法的4倍。虽然ValueTask的内存分配符合预期(更少),但性能开销更大完全出乎意料——我原本以为等待ValueTask的速度会和Task相当甚至更快。
第一次测试(参数固定为1)的结果:
| Method | Mean | Error | StdDev | Min | Max | Median | Allocated |
|---|---|---|---|---|---|---|---|
| AsyncTask | 1.905 ms | 0.5159 ms | 0.0283 ms | 1.881 ms | 1.936 ms | 1.898 ms | 73 B |
| AsyncValueTask | 8.089 ms | 1.1259 ms | 0.0617 ms | 8.018 ms | 8.132 ms | 8.117 ms | 9 B |
测试代码如下:
[ShortRunJob] [MinColumn, MaxColumn, MeanColumn, MedianColumn] [MemoryDiagnoser] [MarkdownExporter] public class AsyncMethodCallsBench { [Benchmark] public async Task<int> AsyncTask() { var sum = 0; for (int i = 0; i < 1_000_000; i++) { sum += await Foo.AsyncTaskMethod(1); } return sum; } [Benchmark] public async ValueTask<int> AsyncValueTask() { var sum = 0; for (int i = 0; i < 1_000_000; i++) { sum += await Foo.AsyncValueTaskMethod(1); } return sum; } } public class Foo { [MethodImpl(MethodImplOptions.NoInlining)] public static Task<int> AsyncTaskMethod(int arg) => Task.FromResult<int>(arg * 2); [MethodImpl(MethodImplOptions.NoInlining)] public static ValueTask<int> AsyncValueTaskMethod(int arg) => ValueTask.FromResult<int>(arg * 2); }
编辑补充:
后来发现Task.FromResult()会缓存部分原始值,于是把参数改为循环变量i,测试结果如下:
| Method | Mean | Error | StdDev | Min | Max | Median | Gen0 | Allocated |
|---|---|---|---|---|---|---|---|---|
| AsyncTask | 3.736 ms | 0.2676 ms | 0.0147 ms | 3.721 ms | 3.750 ms | 3.736 ms | 4300.7813 | 71999714 B |
| AsyncValueTask | 8.151 ms | 1.0713 ms | 0.0587 ms | 8.108 ms | 8.218 ms | 8.126 ms | - | 9 B |
此时Task版本的内存分配大幅增加,但速度仍比ValueTask版本快2倍。
核心原因:同步场景下ValueTask的await路径更复杂
1. Task.FromResult的特殊优化
Task.FromResult<T>返回的是已完成的缓存Task实例(当T为值类型且结果常见时,CLR会复用实例),即使改成参数i每次创建新Task,await已完成Task的路径在.NET里有高度优化——JIT会识别到await的是已完成Task,直接同步执行后续代码,几乎没有额外开销。
2. ValueTask的await额外开销
对于ValueTask.FromResult<T>返回的已完成ValueTask,await它的路径比Task要复杂:
ValueTask是结构体,await时需要先检查它的IsCompleted状态,然后调用GetAwaiter(),这个过程涉及结构体的拷贝和状态判断;- 相比之下,
Task的await路径因为被JIT深度优化,对于已完成的Task,编译器会生成更直接的代码,跳过很多状态检查步骤; - 另外,
ValueTask的awaiter没有Task的awaiter那么多的JIT优化,尤其是在同步完成的场景下,ValueTask的处理逻辑更多。
3. 循环放大了差异
测试是循环100万次,每次的微小差异会被放大。Task的同步await路径几乎是“零开销”,而ValueTask每次都要多做几个步骤,累计下来就形成了数倍的性能差距。
什么时候用ValueTask才合适?
ValueTask的设计目标是减少异步操作成功完成时的内存分配,但它的优势体现在真正异步的场景(比如I/O操作):
- 当操作大概率会异步完成时,
ValueTask避免了分配Task实例的开销; - 但在同步完成的高频场景下,
Task的JIT优化带来的性能收益会超过ValueTask的内存优势,此时用Task反而更快。
内容的提问来源于stack exchange,提问作者zigzag
相关产品推荐
相关产品推荐

