📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

How do x64 and Arm64 CPU architectures differ, and what does a .NET team need to check before running services on Arm64 servers or developing on Apple Silicon?

CISC versus RISC design, instruction sets, memory ordering models and SIMD differences between x64 and Arm64, and the practical checklist for .NET: RIDs, native dependencies, multi-arch container images, concurrency correctness, performance testing and cost.

Mid-Level Theory Intermediate arm64 azure containers cpu cpu-architecture x64

🎯 Key points

  • x64 is a CISC-heritage, variable-length instruction set decoded into micro-ops; Arm64 is a RISC load/store design with fixed 4-byte instructions and 31 general-purpose registers
  • Both are little-endian and 64-bit for .NET purposes; IL and pure managed code run unchanged because the JIT targets the CPU it runs on
  • Arm64 has a weaker memory ordering model than x64 (TSO), so data races that 'worked' on x64 can fail; use lock, Volatile, Interlocked, Lazy<T> or concurrent collections, which emit the right barriers on each platform
  • Native dependencies need arm64 builds for each RID (linux-arm64, osx-arm64, win-arm64); missing ones fail at run time with DllNotFoundException or BadImageFormatException
  • Container images must be multi-arch; cross-compile with FROM --platform=$BUILDPLATFORM and dotnet publish -a $TARGETARCH, or use the SDK's PublishContainer with ContainerRuntimeIdentifiers
  • SIMD differs: SSE/AVX2/AVX-512 on x64 versus 128-bit NEON/AdvSimd on Arm64; the cross-platform Vector128<T> and vectorised BCL APIs map to both, while X86 or Arm intrinsics need IsSupported guards and a scalar fallback
  • New native binaries are a supply-chain change: verify their source and signatures, scan them, and check that APM agents and security tooling support Arm64
  • Arm64 VMs (Azure Cobalt and Ampere-based sizes, AWS Graviton) often give better price-performance; load-test cost per request at your latency target and profile with perf, dotnet-trace and dotnet-counters, which all support Arm64

⏱ 30-second answer

x64 comes from a CISC lineage with variable-length instructions and strong memory ordering. Arm64 is a RISC load/store design with fixed 4-byte instructions, more registers, and a weaker memory model. For .NET, IL and pure managed code run unchanged because the JIT targets whichever CPU it's on. What needs checking: native dependencies must have arm64 builds, container images must be multi-arch, RIDs such as linux-arm64 must be published, hand-written SIMD must have Arm64 or cross-platform paths, and code with data races that happened to work on x64 can break on Arm64. Then benchmark, because Arm64 VMs such as Azure's Cobalt-based sizes often give better price-performance for web workloads.

🗣 Full answer (senior)

Architectural differences

x64 (AMD64) Arm64 (AArch64)
Heritage CISC (x86), decoded into RISC-like micro-ops internally RISC
Instruction length 1–15 bytes Fixed 4 bytes
Memory operands Arithmetic can use memory directly Load/store only
General-purpose registers 16 31
Memory ordering Strong (TSO): stores are seen in order Weak: loads and stores can be reordered unless barriers are used
SIMD SSE, AVX2, AVX-512 (up to 512-bit) NEON/AdvSIMD (128-bit), SVE on some chips
Endianness Little Little (as used by all .NET platforms)
Where you meet it Most cloud VMs, Intel/AMD laptops Apple Silicon Macs, Windows on Arm laptops, Azure Cobalt and Ampere VMs, AWS Graviton, Raspberry Pi

What changes for a .NET app

Usually nothing for pure managed code. The same .dll runs on both because the JIT produces native code for the CPU it's running on.

Things to check:

  1. Native dependencies: packages that ship native libraries (database drivers with native parts, image processing, compression, PDF, ML runtimes) need runtimes/linux-arm64/native/... assets. Missing ones fail at run time with DllNotFoundException or BadImageFormatException.
  2. Publishing: self-contained, single-file, ReadyToRun and Native AOT builds are per RID: dotnet publish -r linux-arm64 (or -a arm64).
  3. Containers: official images such as mcr.microsoft.com/dotnet/aspnet:10.0 are multi-arch manifests. Build your image for the target (docker buildx build --platform linux/arm64,linux/amd64), and use the SDK's cross-compilation pattern (FROM --platform=$BUILDPLATFORM for the build stage) so builds don't run under slow emulation.
  4. Concurrency correctness: Arm64 can reorder memory operations that x64 never would. Code that publishes data between threads without lock, Volatile, Interlocked or concurrent collections may have bugs that only show on Arm64.
  5. SIMD and intrinsics: code using System.Runtime.Intrinsics.X86 (for example Avx2) needs an AdvSimd path or should use the cross-platform Vector128<T> APIs. Always keep a scalar fallback.
  6. Tooling: profilers, APM agents and native debugging tools need Arm64 support. Check before migrating.
  7. Performance and cost: benchmark real workloads; many web APIs run as fast or faster per dollar, but some numeric code tuned for AVX-512 may be slower.

Developer machines

On Apple Silicon, install the Arm64 .NET SDK. The x64 SDK runs under Rosetta 2 but is slower and confuses tooling when both are installed. Docker on Apple Silicon runs arm64 images natively and amd64 images under emulation, which is slow and occasionally behaves differently.

Production example

A team moved a fleet of ASP.NET Core APIs to Azure Arm64 VMs for about 30% lower cost. Most services moved unchanged. One failed on start-up with DllNotFoundException because an image library shipped no linux-arm64 native asset; it was replaced with a fully managed library. Another showed rare corrupted cache entries under load: a hand-written lazy initialisation without Volatile/Lazy<T> that had never failed on x64. Replacing it with Lazy<T> fixed it.

🔍 What the interviewer is checking

Checks architecture awareness with practical deployment consequences.

Green flags: explains CISC/RISC and memory-model differences, knows managed code is portable, lists native assets, RIDs, multi-arch images, SIMD portability and concurrency risks, proposes a measured migration.

Red flags: "Arm is just slower" or "no difference at all"; unaware of native dependencies; doesn't know how to publish for another architecture.

📚 Learn in depth

1. Memory ordering, the subtle difference

Thread A (writer)              Thread B (reader)
data = 42;                     if (ready)
ready = true;                      Console.WriteLine(data);
  • On x64, stores become visible in program order, so B effectively never sees ready == true with stale data, as long as the JIT doesn't reorder (and it may).
  • On Arm64, the hardware itself may make ready visible before data, so B can print 0.
  • Correct code uses Volatile.Write(ref ready, true) and Volatile.Read(ref ready), a lock, Interlocked, or higher-level types (Lazy<T>, ConcurrentDictionary, Channel<T>). The .NET memory model documents the guarantees; anything beyond them is a race on every platform, but Arm64 exposes it more often.

2. Native assets in NuGet packages

MyPackage.nupkg
|-- lib/net8.0/MyPackage.dll                   managed, works everywhere
`-- runtimes/
    |-- win-x64/native/mylib.dll
    |-- linux-x64/native/libmylib.so
    `-- linux-arm64/native/libmylib.so         <- must exist for Arm64

Check with: unpack the .nupkg (it's a ZIP) or look at the package's "Frameworks" and documentation; then run the app's test suite on an Arm64 agent.

3. Building multi-arch container images

FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:10.0 AS build
ARG TARGETARCH
WORKDIR /src
COPY . .
RUN dotnet publish -c Release -a $TARGETARCH -o /app

FROM mcr.microsoft.com/dotnet/aspnet:10.0
WORKDIR /app
COPY --from=build /app .
ENTRYPOINT ["dotnet", "Api.dll"]
docker buildx build --platform linux/amd64,linux/arm64 -t myregistry/api:1.2.3 --push .

The build stage runs natively on the build machine and cross-compiles with -a $TARGETARCH; only the final stage is per-architecture. Alternatively, dotnet publish /t:PublishContainer with ContainerRuntimeIdentifiers builds images without a Dockerfile.

4. SIMD portability

API Portability
Vector128<T>, Vector256<T>, Vector<T> operators Cross-platform; the JIT maps them to SSE/AVX or NEON
System.Runtime.Intrinsics.X86.Avx2 x64 only; guard with Avx2.IsSupported
System.Runtime.Intrinsics.Arm.AdvSimd Arm64 only; guard with AdvSimd.IsSupported

Vector256<T> on Arm64 is emulated with two 128-bit operations (Vector256.IsHardwareAccelerated is false), so Vector128<T> is the portable sweet spot.

5. Migration checklist

  1. Inventory native dependencies and tools (APM agents, profilers).
  2. Add an Arm64 CI leg that runs the full test suite.
  3. Publish multi-arch images.
  4. Run load tests on the target VM size and compare latency, throughput and cost.
  5. Roll out gradually (a canary node pool), with an easy way back to x64.
  6. Review lock-free or hand-rolled concurrency code for memory ordering assumptions.

6. Try it yourself

  1. Print RuntimeInformation.ProcessArchitecture and OSArchitecture on your machine and inside an arm64 container.
  2. Build a multi-arch image with the Dockerfile above and inspect the manifest with docker buildx imagetools inspect.
  3. Write a small method using Vector128 and look at the disassembly on x64 and on Arm64 (an Apple Silicon Mac or an Arm64 VM) with DOTNET_JitDisasm.

💻 Code

namespace ArchitectureAware;

using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;

public static class Program
{
    public static void Main()
    {
        Console.WriteLine($"Process architecture: {RuntimeInformation.ProcessArchitecture}");
        Console.WriteLine($"OS architecture:      {RuntimeInformation.OSArchitecture}");
        Console.WriteLine($"Runtime identifier:   {RuntimeInformation.RuntimeIdentifier}");
        Console.WriteLine($"Vector128 hardware:   {Vector128.IsHardwareAccelerated}");
        Console.WriteLine($"Vector256 hardware:   {Vector256.IsHardwareAccelerated}");
    }
}

// Correct cross-thread publication on every architecture
public sealed class ConfigHolder
{
    private Config? _current;

    public void Publish(Config config) => Volatile.Write(ref _current, config);

    public Config? Current => Volatile.Read(ref _current);
}

public sealed record Config(string Endpoint, int TimeoutSeconds);
# Publish for Arm64 Linux, self-contained, from any machine
dotnet publish -c Release -r linux-arm64 --self-contained

# Or build a container image for both architectures with the SDK (no Dockerfile)
dotnet publish -c Release /t:PublishContainer -p:ContainerRuntimeIdentifiers="linux-x64;linux-arm64"

⚠ Common mistakes

Mistake: Assuming "it's .NET, so it just works" on Arm64. Why it is wrong: managed code does, but native dependencies, agents and hand-written intrinsics may not, and failures appear only at run time. Instead: inventory native assets and tools, and run the full test suite on Arm64 in CI.

Mistake: Pulling in an Arm64 native binary from wherever it can be found. Why it is wrong: a different native build is new code in your supply chain. An unofficial or unmaintained build may lag on security fixes, and a library that is patched on x64 might not be on Arm64. Instead: take native assets only from the official package or vendor, check signatures and SBOM/vulnerability scans for the Arm64 artefacts too, and prefer fully managed alternatives where they exist.

Mistake: Building Arm64 images under QEMU emulation. Why it is wrong: builds become many times slower and occasionally hit emulation bugs. Instead: cross-compile with --platform=$BUILDPLATFORM and -a $TARGETARCH, or build on native Arm64 runners.

Mistake: Relying on x64's strong memory ordering. Why it is wrong: unsynchronised flags and double-checked locking without Volatile can work for years on x64 and fail under load on Arm64. Instead: use lock, Volatile, Interlocked, Lazy<T> or concurrent collections for any cross-thread data, and stress-test on Arm64.

Mistake: Writing x86-only intrinsics without a fallback. Why it is wrong: Avx2 code paths never run on Arm64, so the fallback path (if any) is untested. Instead: prefer cross-platform Vector128<T> APIs and always include a scalar fallback with tests on both architectures.

Mistake: Choosing an architecture on list price alone. Why it is wrong: price-performance depends on the workload; numeric code tuned for AVX-512 may need more Arm64 cores. Instead: load-test the real service on both and compare cost per request at your latency target.

Mistake: Mixing x64 and Arm64 SDKs on an Apple Silicon Mac. Why it is wrong: tools pick up the wrong dotnet on the PATH and builds behave inconsistently. Instead: install only the Arm64 SDK unless you have a specific need for x64 under Rosetta.

🏛 Senior insight: trade-offs

Seniors frame Arm64 adoption as a cost and risk decision. .NET's portability makes the move mostly mechanical, but the risks are concentrated in a few places.

Risk area What can go wrong Mitigation
Native dependencies Start-up failure, missing security patches Inventory runtimes/*/native, official builds only, managed alternatives
Agents and tooling APM, security or profiling agent unsupported Confirm vendor Arm64 support before migrating
Concurrency Latent races exposed by weaker ordering Arm64 CI leg, stress tests, no hand-rolled lock-free code
SIMD Slower or untested paths Vector128<T>, BCL vectorised APIs, scalar fallback tests
Build pipeline Slow emulated builds Cross-compile, native Arm64 runners
Cost / performance Savings smaller than expected Load test, compare cost per request at p95 target

A senior plan includes an Arm64 CI leg, multi-arch images, a canary node pool with an easy way back to x64, and a load-test comparison rather than a big-bang switch. Running Arm64 in CI also hardens the codebase, because it flushes out races that x64 hides.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate mentions publishing for linux-arm64. Why: Checks the project-system mechanics of targeting a specific architecture.
    How to answer

    A runtime identifier, or RID, names the platform you build for, as OS plus architecture: win-x64, linux-x64, linux-musl-x64 for Alpine, linux-arm64 or osx-arm64. A normal framework-dependent app doesn't need one. It stays portable, and packages with native code ship runtimes/<rid>/ folders that the runtime picks from at start-up. You set a RID when the output must be platform-specific: self-contained, single-file, ReadyToRun or Native AOT. Since .NET 8 the SDK only understands portable RIDs, so distro-specific ones like ubuntu.22.04-x64 no longer work. Also since .NET 8, -r alone doesn't make the app self-contained.

  2. When: Candidate talks about building per-RID artefacts or container images. Why: Checks how publish options such as self-contained, ReadyToRun and Native AOT interact with the target architecture.
    How to answer

    By default dotnet publish gives a framework-dependent folder that needs the .NET runtime on the server. Self-contained (--self-contained -r linux-x64) bundles the runtime, so nothing needs installing, but the output is bigger and you patch the runtime by redeploying. On top of that: single-file packs everything into one executable, trimming removes unused code, ReadyToRun precompiles to speed up start-up, and Native AOT compiles to a native binary with very fast start-up and low memory but no dynamic code generation. The SDK can also build a container image directly. I choose based on where it runs, start-up and memory needs, and whether the libraries are trim- and AOT-safe, and I measure the result.

  3. When: Candidate mentions x86-only intrinsics needing a fallback. Why: Probes SIMD portability between AVX and NEON.
    How to answer

    SIMD (single instruction, multiple data) lets one CPU instruction operate on several values at once, held in a vector register: a 128-bit register holds four ints, a 256-bit one holds eight. x64 offers SSE, AVX2 and AVX-512; Arm64 offers NEON. .NET exposes this through portable types (Vector128<T>, Vector256<T>, Vector512<T> and the size-agnostic Vector<T>), plus platform-specific intrinsics. First, use what's already vectorised: Span methods like IndexOf and SequenceEqual, and TensorPrimitives. Hand-write SIMD only for a measured hot loop over contiguous primitive data, with a scalar tail and a fallback when IsHardwareAccelerated is false.

🛠 Real-world problems: how you handle them

Migrating ASP.NET Core APIs to Arm64 VMs for lower cost
Situation
A company running about 40 ASP.NET Core services on x64 VMs in Azure Australia East wanted to cut compute costs by moving to Arm64 VM sizes.
Signals
After the first canary deployment, one service failed on start-up with `DllNotFoundException`, and another showed rare corrupted entries in an in-memory cache under load that had never appeared on x64.
Investigation
Checked the failing service's packages and found an image-processing library with no linux-arm64 native asset. For the cache issue, reviewed the code path and found a hand-written lazy initialisation using a plain field and a flag without `Volatile` or locking; reproduced it with a stress test on an Arm64 VM.
Resolution
Replaced the image library with a fully managed alternative, replaced the hand-written initialisation with `Lazy<T>`, built multi-arch images with buildx cross-compilation, and rolled out service by service through a canary node pool.
Prevention
Added an Arm64 CI leg running all tests, a dependency check for native assets, a review rule against hand-rolled lock-free code, and a cost-per-request dashboard comparing node pools.

How to tell it (STAR)

**Situation:** At [company], we wanted to cut cloud compute costs by moving [40] ASP.NET Core services to Arm64 VMs. **Task:** I led the migration and had to avoid outages. **Action:** I set up multi-arch builds and an Arm64 CI leg, then moved services through a canary pool. Two problems surfaced: a library without an Arm64 native binary, which we replaced, and a latent race in a hand-written lazy initialiser that only showed up on Arm64's weaker memory ordering, which I fixed with `Lazy<T>` after reproducing it under stress. **Result:** All services moved over [N weeks] with no customer-facing incidents and about [30%] lower compute cost, and the Arm64 CI leg has caught [N] more concurrency bugs since.

↩ Usually asked after

More in CPU

Last updated 23 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me