Here’s a single, self-contained console app that lets you experience the difference between:
- 🚫 Without
ValueTask– usingasync Task<int>(allocates aTaskeven on fast sync path) - ✅ With
ValueTask– returning synchronously without allocating when value is cached
You’ll see timing + GC stats for both in the same run.
1️⃣ Full Code – Program.cs
This is <strong>100% self-contained</strong> – no extra packages needed.Code language: HTML, XML (xml)
2️⃣ Optional – Minimal .csproj
If you want to control the target framework explicitly, create ValueTaskDemo.csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
Code language: HTML, XML (xml)
You can switch to net9.0 or net10.0 if you’re targeting .NET 9/10 SDK.
3️⃣ Step-by-Step: How to Run It
Step 1 – Create a new console project
dotnet new console -n ValueTaskDemo
cd ValueTaskDemo
Code language: JavaScript (javascript)
Step 2 – Replace Program.cs
- Open
Program.csin your editor - Delete everything
- Paste the full code from section 1️⃣
(Optionally replace the generated .csproj with the file in 2️⃣.)
Step 3 – Build and run in Release mode
dotnet run -c Release
You’ll see output similar to:
=========================================
ValueTask Demo – Task vs ValueTask
=========================================
Iterations : 5,000,000
Cache size : 1,000
Scenario : All calls hit the FAST (cached) path.
Warming up (small runs)...
--- Warmup – async Task<int> ---
Iterations : 500,000
Time Elapsed : 80 ms
GC Gen0 : 10
GC Gen1 : 0
GC Gen2 : 0
Managed Memory Δ : 3.20 MB
Checksum (ignore) : 499500000
--- Warmup – ValueTask<int> ---
Iterations : 500,000
Time Elapsed : 45 ms
GC Gen0 : 3
GC Gen1 : 0
GC Gen2 : 0
Managed Memory Δ : 0.70 MB
Checksum (ignore) : 499500000
=========== REAL TESTS (Release) ==========
--- WITHOUT ValueTask – async Task<int> ---
Iterations : 5,000,000
Time Elapsed : 800 ms
GC Gen0 : 100
GC Gen1 : 5
GC Gen2 : 0
Managed Memory Δ : 30.50 MB
Checksum (ignore) : 4995000000
--- WITH ValueTask – ValueTask<int> ---
Iterations : 5,000,000
Time Elapsed : 430 ms
GC Gen0 : 15
GC Gen1 : 0
GC Gen2 : 0
Managed Memory Δ : 5.10 MB
Checksum (ignore) : 4995000000
Code language: HTML, XML (xml)
(Exact numbers will vary per machine, but the pattern should be similar.)
4️⃣ How to “Experience” and Interpret the Results
Focus on these for each scenario:
- ⏱
Time Elapsed - 🗑
GC Gen0 / Gen1 / Gen2 - 💾
Managed Memory Δ
🔴 Scenario 1 – WITHOUT ValueTask (async Task)
public async Task<int> GetValueAsync(int key)
{
if (_cache.TryGetValue(key, out var value))
{
return value; // looks sync, but method is async
}
await Task.Delay(1).ConfigureAwait(false);
return key;
}
Code language: JavaScript (javascript)
Even when the value is immediately available in cache, the compiler:
- Generates an async state machine
- Allocates a Task object for each call
In a tight loop with millions of calls, you’ll see:
- Higher
Time Elapsed - More
GC Gen0collections - Higher
Managed Memory Δ(more allocations)
🟢 Scenario 2 – WITH ValueTask (ValueTask)
public ValueTask<int> GetValueAsync(int key)
{
if (_cache.TryGetValue(key, out var value))
{
return new ValueTask<int>(value); // completes synchronously, no Task allocation
}
return new ValueTask<int>(SlowPathAsync(key)); // real Task only on slow path
}
Code language: PHP (php)
Now:
- For the fast path (cache hit), we:
- Do not create a
Task - Do not create a state machine
- Do not create a
- We only use a real
Task<int>on the slow path (SlowPathAsync), which we’re not hitting in this benchmark.
In the output, you should see:
- Lower elapsed time (less allocation + less GC work)
- Much fewer Gen0 collections
- Smaller Managed Memory Δ
This is exactly the use case where ValueTask shines:
A method that is often synchronous but must expose an async API.
5️⃣ Tweaks to Make the Effect More Visible
You can tune:
iterations(e.g.,10_000_000if your machine is strong)cacheSize(smaller cache keeps hit ratio at 100%)
const int iterations = 10_000_000;
const int cacheSize = 100;
Code language: JavaScript (javascript)
This increases the number of fast-path calls and magnifies the Task vs ValueTask difference.
I’m a DevOps/SRE/DevSecOps/Cloud Expert passionate about sharing knowledge and experiences. I have worked at Cotocus. I share tech blog at DevOps School, travel stories at Holiday Landmark, stock market tips at Stocks Mantra, health and fitness guidance at My Medic Plus, product reviews at TrueReviewNow , and SEO strategies at Wizbrand.
Do you want to learn Quantum Computing?
Please find my social handles as below;
Rajesh Kumar Personal Website
Rajesh Kumar at YOUTUBE
Rajesh Kumar at INSTAGRAM
Rajesh Kumar at X
Rajesh Kumar at FACEBOOK
Rajesh Kumar at LINKEDIN
Rajesh Kumar at WIZBRAND