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
This article provides a very clear explanation of how
ValueTaskcan significantly improve memory efficiency in .NET applications, especially in scenarios where asynchronous operations often complete synchronously. By reducing unnecessary heap allocations and lowering GC pressure,ValueTaskbecomes a powerful tool for developers working on high-performance or high-frequency code paths. I appreciate how the blog highlights not only the benefits but also the correct usage patterns, helping readers understand whenValueTaskis appropriate and when a regularTaskis still the better choice. Overall, this is a very practical and insightful guide for anyone looking to optimize memory usage and improve the responsiveness of their .NET applications.