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 Results

For teams considering ARM64 development or CI infrastructure, the useful questions are:

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
Parallelism

Therefore:

Build Time ≠ CPU Architecture Alone

A 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.sln

The project should be large enough to exercise the build system but stable enough to produce repeatable results.

Record the exact SDK:

dotnet --info

For 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 restore

Build

dotnet build \
  --configuration Release \
  --no-restore

Test

dotnet test \
  --configuration Release \
  --no-build

This 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 sec

These 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
  ↓
Build

Later run:

Restore
  ↓
Packages already cached
  ↓
Build

The second run may be much faster.

Therefore, decide whether the benchmark is measuring:

Cold build

or:

Warm build

and 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-arm64

The 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.TotalSeconds

This 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 5

Then compare distributions rather than relying only on one value.

For example:

Median build time
Minimum build time
Maximum build time
Variation between runs

The 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 changes

Therefore, a result such as:

ARM64 = 70 seconds
x64   = 72 seconds

does 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 version

This gives the result useful context.

Testing Performance

The test stage can expose architecture-specific behavior.

Run:

dotnet test --configuration Release --no-build

If 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 compatibility

A 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++ components

Check whether every dependency supports ARM64.

A package may support:

x64

but not:

ARM64

In 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-arm64

and:

dotnet publish \
  --configuration Release \
  --runtime win-x64

This 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 architecture

or:

Published target architecture

These 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 emulation

This is different from:

ARM64 GitHub runner
        vs
x64 GitHub runner

Do 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, AddressWidth

You can also inspect the .NET environment:

dotnet --info

Store this information alongside benchmark results.

Benchmark Result Format

A useful result table could look like:

MetricARM64x64
RestoreMeasuredMeasured
BuildMeasuredMeasured
TestMeasuredMeasured
PublishMeasuredMeasured
TotalMeasuredMeasured

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 --info

Check Package Compatibility

Inspect dependencies for ARM64 support.

Check Native Libraries

Identify packages that contain native binaries.

Check Runtime Identifiers

Verify the requested RID:

win-arm64

versus:

win-x64

Check Build Scripts

Look for architecture-specific paths or assumptions.

For example:

C:\Program Files\SomeTool\

may not be correct across all environments.

Best Practices

  1. Benchmark the same commit on both architectures.

  2. Use the same .NET SDK version.

  3. Record runner and OS details.

  4. Separate restore, build, test, and publish measurements.

  5. Run multiple iterations.

  6. Keep cache conditions consistent.

  7. Record native dependency compatibility.

  8. Distinguish host architecture from target architecture.

  9. Compare median and variation rather than a single run.

  10. Treat small performance differences cautiously.

  11. Re-run benchmarks when runner images or SDK versions change.

Advantages and Disadvantages

ARM64 CI

Advantages

Disadvantages

x64 CI

Advantages

Disadvantages

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.