
If you’ve been reading about .NET 8, you may have heard that Dynamic Profile-Guided Optimization (PGO) is now enabled by default.
Sounds fancy, right? Let’s break it down in plain English.
Imagine you’re teaching a student how to solve math problems. At first, they try many ways. Over time, they learn which steps work best and start solving faster.
That’s exactly what PGO does for your app.
There are two main flavors:
Type | How it Works | Example | Downsides |
---|---|---|---|
Static PGO | You collect data once (e.g., profiling during tests). Compiler uses it to optimize ahead of time. | Training a runner before the race. | If real-world usage is different, optimizations may not fit well. |
Dynamic PGO | App learns while running. Runtime adjusts optimizations on the fly. | Runner adapts pace based on real race conditions. | Slight overhead at the start, but pays off quickly. |
👉 In .NET 8, Dynamic PGO is the default. That means every app can benefit automatically.
Here’s what you gain without lifting a finger:
By default, .NET 8 turns on Dynamic PGO for you.
So in most cases → you don’t need to touch anything.
But sometimes you may want to:
Here’s how 👇
Every .NET project has a .csproj
file (for example MyApp.csproj
).
Inside this file, there’s a <PropertyGroup>
section where you can add settings.
To enable PGO explicitly:
<PropertyGroup>
<TieredCompilation>true</TieredCompilation>
<TieredPGO>true</TieredPGO>
</PropertyGroup>
What this means:
TieredCompilation
→ allows .NET to recompile methods at runtime (faster startup + better performance later).TieredPGO
→ tells .NET to use profile-guided optimization dynamically.👉 If you already have .NET 8, both are usually true
by default. But setting them makes sure.
You can also control it without touching project files.
This is handy if you just want to test something quickly.
$env:DOTNET_TieredPGO=1
dotnet run
export DOTNET_TieredPGO=1
dotnet run
Values you can use:
1
→ Enable Dynamic PGO0
→ Disable Dynamic PGO👉 This only applies to the current session/command window. Once you close it, the setting is gone.
Dynamic PGO isn’t something you can “see” directly, but here’s how you can check:
DOTNET_TieredPGO=1
.For example, an ASP.NET Core app might handle 10–20% more requests per second with PGO enabled.
✅ In short:
We’ll create a console app that:
Open your terminal and run:
dotnet new console -n PgoDemo
cd PgoDemo
Replace Program.cs
with this:
using System;
using System.Diagnostics;
class Program
{
static void Main()
{
Console.WriteLine("Dynamic PGO Demo 🚀");
Console.WriteLine("Running calculation...");
var stopwatch = Stopwatch.StartNew();
long result = CalculatePrimes(2_000_000);
stopwatch.Stop();
Console.WriteLine($"Result: {result}");
Console.WriteLine($"Time taken: {stopwatch.ElapsedMilliseconds} ms");
}
static long CalculatePrimes(int limit)
{
long sum = 0;
for (int number = 2; number < limit; number++)
{
if (IsPrime(number))
sum += number;
}
return sum;
}
static bool IsPrime(int n)
{
if (n < 2) return false;
for (int i = 2; i * i <= n; i++)
{
if (n % i == 0) return false;
}
return true;
}
}
👉 This code:
Perfect for stressing the CPU.
Disable Dynamic PGO:
$env:DOTNET_TieredPGO=0
dotnet run -c Release
export DOTNET_TieredPGO=0
dotnet run -c Release
⏱ Note the time taken.
Enable Dynamic PGO:
$env:DOTNET_TieredPGO=1
dotnet run -c Release
export DOTNET_TieredPGO=1
dotnet run -c Release
⏱ Compare the time again.
IsPrime
) and re-optimizes.
Run Mode | Time Taken (ms) |
---|---|
Without PGO | 4200 ms |
With Dynamic PGO | 3400 ms |
👉 That’s ~19% faster. And you didn’t change the code at all.
Dynamic PGO is like giving your app a smart brain. It learns, adapts, and runs smoother the more it’s used.
The best part? You don’t need to do anything extra. Just upgrade to .NET 8 and enjoy free performance gains.
So, whether you’re building APIs, crunching data, or running background services, Dynamic PGO makes sure your app doesn’t just run… it runs smarter.