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.
For a fair test, use the same cache strategy on both runner types.
Build Performance
The compilation stage is generally more CPU-intensive than package restore.
Measure:
$buildTime = Measure-Command {
dotnet build `
--configuration Release `
--no-restore
}
Write-Host "Build: $($buildTime.TotalSeconds) seconds"Also record:
Project count
Target framework
Configuration
Architecture
SDK versionThis gives the result useful context.
Testing Performance
The test stage can expose architecture-specific behavior.
Run:
dotnet test --configuration Release --no-buildIf the test suite uses native dependencies, browser automation, database drivers, or platform-specific components, architecture compatibility may become more important than raw build time.
For example:
Managed .NET tests
+
Native dependency
↓
Architecture compatibilityA project that builds successfully may still fail during test execution because of a native dependency.
Native Dependencies Need Special Attention
Pure managed .NET applications generally have fewer architecture-specific concerns than applications that rely on native libraries.
Examples include:
Native database drivers
Image-processing libraries
Hardware integrations
Native Windows APIs
C/C++ componentsCheck whether every dependency supports ARM64.
A package may support:
x64but not:
ARM64In that situation, the build benchmark becomes an important compatibility test rather than simply a performance comparison.
Runtime Identifier Considerations
Applications that publish architecture-specific binaries may use a Runtime Identifier.
For example:
dotnet publish \
--configuration Release \
--runtime win-arm64and:
dotnet publish \
--configuration Release \
--runtime win-x64This is different from simply building the application on an ARM64 or x64 machine.
A complete architecture benchmark should clearly state whether it measures:
Build host architectureor:
Published target architectureThese are not necessarily the same thing.
Comparing Native ARM64 and Emulation
Windows on ARM systems can run x64 applications through emulation.
That creates another possible comparison:
Native ARM64 .NET
vs
x64 application under emulationThis is different from:
ARM64 GitHub runner
vs
x64 GitHub runnerDo not combine these experiments.
If the goal is to evaluate native ARM64 CI, the benchmark should specifically measure native ARM64 workloads.
Architecture Verification
Do not assume the runner architecture based only on its operating system name.
Record it explicitly.
PowerShell can expose system information:
Get-CimInstance Win32_Processor |
Select-Object Name, AddressWidthYou can also inspect the .NET environment:
dotnet --infoStore this information alongside benchmark results.
Benchmark Result Format
A useful result table could look like:
| Metric | ARM64 | x64 |
|---|---|---|
| Restore | Measured | Measured |
| Build | Measured | Measured |
| Test | Measured | Measured |
| Publish | Measured | Measured |
| Total | Measured | Measured |
Do not fill this table with fabricated numbers.
The values should come from the actual CI runs.
Common Mistakes
Comparing Different SDK Versions
A newer SDK can change build performance independently of architecture.
Running Only Once
A single execution is vulnerable to normal CI variability.
Ignoring Cache State
Warm and cold builds can produce very different results.
Comparing Different Workloads
Both runners must build the same commit with the same configuration.
Ignoring Native Dependencies
Architecture compatibility can become the primary issue for native libraries.
Treating Small Differences as Proof
A two-second difference may be meaningless if normal runner variation is larger.
Measuring Only Build Time
Restore, tests, packaging, and publishing can also affect CI duration.
Troubleshooting ARM64 Build Failures
If the project succeeds on x64 but fails on ARM64, investigate in this order.
Check the .NET SDK
dotnet --infoCheck Package Compatibility
Inspect dependencies for ARM64 support.
Check Native Libraries
Identify packages that contain native binaries.
Check Runtime Identifiers
Verify the requested RID:
win-arm64versus:
win-x64Check Build Scripts
Look for architecture-specific paths or assumptions.
For example:
C:\Program Files\SomeTool\may not be correct across all environments.
Best Practices
Benchmark the same commit on both architectures.
Use the same .NET SDK version.
Record runner and OS details.
Separate restore, build, test, and publish measurements.
Run multiple iterations.
Keep cache conditions consistent.
Record native dependency compatibility.
Distinguish host architecture from target architecture.
Compare median and variation rather than a single run.
Treat small performance differences cautiously.
Re-run benchmarks when runner images or SDK versions change.
Advantages and Disadvantages
ARM64 CI
Advantages
Tests native ARM64 compatibility.
Useful for teams supporting Windows ARM devices.
Can expose architecture-specific dependency problems earlier.
Provides a native ARM64 build environment.
Disadvantages
Some dependencies may still have limited ARM64 support.
Existing build scripts may contain x64 assumptions.
Results can vary with runner infrastructure.
Architecture-specific testing increases CI matrix complexity.
x64 CI
Advantages
Broad compatibility with existing Windows tooling and native dependencies.
Mature ecosystem for Windows development.
Often represents the established baseline for existing applications.
Disadvantages
Does not validate native ARM64 compatibility.
Can hide architecture-specific issues.
May not represent the environment used by ARM64 developers.
Conclusion
Comparing Windows ARM64 and x64 GitHub Actions runners is useful, but the benchmark needs to be designed carefully.
The right experiment is:
Same Repository
↓
Same SDK
↓
Same Dependencies
↓
Same Build Configuration
↓
ARM64 Runner ─────┐
├── Compare
x64 Runner ───────┘Measure restore, compilation, testing, packaging, and publishing separately. Run multiple iterations and record cache conditions and runner details.
Most importantly, do not assume that a small timing difference represents a meaningful architectural advantage. Hosted CI infrastructure naturally introduces variability, and dependency compatibility can be more important than raw build speed.
For .NET teams, the strongest reason to introduce Windows ARM64 CI is not simply to make builds faster. It is to validate that the application, its dependencies, and its automation workflow genuinely support ARM64 while providing real performance data for the workloads that matter.

Join the conversation! Your thoughts help the community grow.