As .NET applications increasingly target multiple CPU architectures, build infrastructure also needs to support those architectures reliably. Windows on ARM64 is particularly relevant for developers using ARM-based Windows devices and for teams evaluating native ARM64 build environments.
GitHub Actions provides Windows ARM64 runner options for supported workflows. This creates an opportunity to compare .NET build performance on Windows ARM64 and x64 runners using a controlled benchmark.
The important point is that a meaningful comparison should measure the same repository, SDK, dependencies, build configuration, and workload on both architectures. A single build time is not enough to establish that one architecture is universally faster.
Why Compare ARM64 and x64?
.NET supports multiple processor architectures, including ARM64 and x64.
A simplified build comparison looks like:
Same .NET Repository
|
+-------------------+
| |
v v
Windows ARM64 Windows x64
Runner Runner
| |
v v
.NET Build .NET Build
| |
+---------+---------+
|
v
Compare ResultsFor teams considering ARM64 development or CI infrastructure, the useful questions are:
Does the project build successfully on both architectures?
How long does restore take?
How long does compilation take?
Do native dependencies behave differently?
Are test execution times different?
Does the project require architecture-specific changes?
ARM64 and x64 Are Different Execution Environments
The first mistake in benchmarking is treating CPU architecture as the only variable.
A build is affected by:
CPU
Memory
Storage
Operating System
.NET SDK
MSBuild
NuGet cache
Project size
Dependencies
Build configuration
ParallelismTherefore:
Build Time ≠ CPU Architecture AloneA fair benchmark must control as many of these variables as possible.
Preparing a .NET Benchmark Project
A representative solution might contain:
BenchmarkSolution/
|
+-- WebApp/
+-- Application/
+-- Infrastructure/
+-- Tests/
+-- BenchmarkSolution.slnThe project should be large enough to exercise the build system but stable enough to produce repeatable results.
Record the exact SDK:
dotnet --infoFor CI, the workflow should explicitly select the required SDK version.
For example:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'Use the SDK version required by the project rather than copying this version blindly.
Defining the Benchmark
A useful benchmark separates the major build phases.
Restore
dotnet restoreBuild
dotnet build \
--configuration Release \
--no-restoreTest
dotnet test \
--configuration Release \
--no-buildThis separation makes it easier to determine where an architecture difference occurs.
For example:
ARM64
Restore: 35 sec
Build: 70 sec
Tests: 45 sec
x64
Restore: 30 sec
Build: 68 sec
Tests: 47 secThese numbers are only illustrative. A real benchmark must collect them from the actual environment.
Avoid Benchmarking a Warm Cache Accidentally
NuGet and build caches can substantially affect results.
Consider two scenarios.
First run:
Restore
↓
Download packages
↓
BuildLater run:
Restore
↓
Packages already cached
↓
BuildThe second run may be much faster.
Therefore, decide whether the benchmark is measuring:
Cold buildor:
Warm buildand use the same approach for both architectures.
A GitHub Actions Matrix
A matrix can execute the same benchmark workflow for different runner architectures.
Conceptually:
strategy:
matrix:
runner:
- windows-x64
- windows-arm64The exact runner labels available to a repository depend on GitHub's current runner offerings and account configuration.
A workflow can then select the appropriate runner:
runs-on: ${{ matrix.runner }}The key requirement is that both jobs perform exactly the same build operations.
Example Benchmark Workflow
A simplified structure is:
name: .NET Architecture Benchmark
on:
workflow_dispatch:
jobs:
benchmark:
strategy:
matrix:
runner:
- windows-x64
- windows-arm64
runs-on: ${{ matrix.runner }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Show environment
shell: pwsh
run: |
dotnet --info
Get-CimInstance Win32_Processor |
Select-Object Name, NumberOfCores
- name: Restore
shell: pwsh
run: |
Measure-Command {
dotnet restore
}
- name: Build
shell: pwsh
run: |
Measure-Command {
dotnet build `
--configuration Release `
--no-restore
}
- name: Test
shell: pwsh
run: |
Measure-Command {
dotnet test `
--configuration Release `
--no-build
}This provides a starting point rather than a complete statistical benchmark.
Why Measure-Command Is Useful
PowerShell's Measure-Command can provide elapsed execution time for a command block.
For example:
$buildTime = Measure-Command {
dotnet build `
--configuration Release `
--no-restore
}
$buildTime.TotalSecondsThis allows the workflow to capture a consistent measurement.
For more rigorous benchmarking, store the measurements as artifacts or job outputs and aggregate multiple runs.
Run Multiple Iterations
One build does not provide a reliable benchmark.
A better experiment runs multiple iterations:
ARM64
Run 1
Run 2
Run 3
Run 4
Run 5
x64
Run 1
Run 2
Run 3
Run 4
Run 5Then compare distributions rather than relying only on one value.
For example:
Median build time
Minimum build time
Maximum build time
Variation between runsThe median can be particularly useful when occasional runner noise affects one execution.
Runner Variability Matters
Hosted CI runners are shared infrastructure.
A build can be affected by:
Background load
Storage performance
Network conditions
Package-cache state
Runner image changesTherefore, a result such as:
ARM64 = 70 seconds
x64 = 72 secondsdoes not automatically prove a meaningful performance advantage.
If the difference is small relative to run-to-run variation, the benchmark should report that rather than claiming a definitive winner.
Restore Performance
Restore can be influenced heavily by network and package-cache behavior.
Measure it separately:
$restoreTime = Measure-Command {
dotnet restore
}
Write-Host "Restore: $($restoreTime.TotalSeconds) seconds"If packages are already cached, the benchmark is partly measuring cache performance rather than dependency download performance.
Join the conversation! Your thoughts help the community grow.