ARM64 Windows Program Files census: x64 EXEs still outnumber ARM64
In this article
Reading the PE headers of 957 EXEs under C:\Program Files found 580 x64 files and 315 ARM64 files. That was a little different from what the system felt like at the entry point.
Because the Surface Pro 11th Edition runs ARM64 Windows, I expected most installed executables to be ARM64 too. When I counted the everyday environment as-is on 2026-06-24, the result was the opposite. The machine had 32GB of memory, a Snapdragon X 12-core X1E80100 @ 3.40GHz, and Windows 11 Pro 10.0.26200 ARM64. It was not cleaned up into a special test environment; the usual applications were still installed.
C:\Program Files (x86) was another thing that did not fit the name. It contained 99 ARM64 files and 52 x86 files. The folder name and the binary architecture no longer correspond directly, so I had to discard the loose assumption that “ARM64 Windows means Program Files is mostly ARM64.”
I read only the PE headers
The procedure was just to enumerate *.exe under each target folder and read the PE header's Machine field. I classified 0xAA64 as ARM64, 0x8664 as x64, and 0x014c as x86. Nothing was executed, so installers and application bodies produced no side effects (otherwise this would become application inventory mixed with execution).
Get-ChildItem "C:\Program Files" -Recurse -Filter *.exe -ErrorAction SilentlyContinue |
ForEach-Object { Read-PeMachine $_.FullName }
I used the same approach for tools normally launched from the command line. I found the entry point with where.exe or Get-Command, then read the Machine field if the file was a PE. Wrappers such as npm and VS Code were classified as not-PE at this stage, and I stopped there (following their actual payloads would be enough for another article).
$tools = "python", "node", "npm.cmd", "git", "dotnet", "ffmpeg", "ffprobe", "curl", "tar", "java", "javac", "code.cmd", "pwsh", "powershell", "docker", "gh", "winget", "wsl", "where.exe", "notepad"
$tools | ForEach-Object { Get-Command $_ | Select-Object Name, Source }
The folder scan stopped at a limit of 4,000 files. The counts here were 957 for C:\Program Files, 154 for C:\Program Files (x86), and 225 for LocalAppData\Programs, so these results stayed below the limit. On a much larger installation, the same script could become a capped sample rather than a full census.
The 20 frequently used tools leaned strongly toward ARM64
The 20 command-line tools I use regularly contained more ARM64 files than I expected. python, node, git, dotnet, curl, tar, pwsh, powershell, gh, wsl, and notepad were running as ARM64. That is probably why everyday use felt “mostly native.”
The video tools and Java were x64. ffmpeg and ffprobe came from the winget package Gyan.FFmpeg 8.1.2-full_build; java and javac came from Eclipse Adoptium jdk-8.0.492.9-hotspot. If I overlook this and measure video processing or an old Java program, I may think I am reading an ARM64-native benchmark while actually including x64 emulation.
| Tool | Architecture | Path |
|---|---|---|
| python | ARM64 | C:\Users\akirasakai\AppData\Local\Programs\Python\Python312-arm64\python.exe |
| node | ARM64 | C:\Program Files\nodejs\node.exe |
| npm.cmd | not-PE | C:\Program Files\nodejs\npm.cmd |
| git | ARM64 | C:\Program Files\Git\cmd\git.exe |
| dotnet | ARM64 | C:\Program Files\dotnet\dotnet.exe |
| ffmpeg | x64 | C:\Users\akirasakai\AppData\Local\Microsoft\WinGet\Packages\Gyan.FFmpeg_Microsoft.Winget.Source_8wekyb3d8bbwe\ffmpeg-8.1.2-full_build\bin\ffmpeg.exe |
| ffprobe | x64 | C:\Users\akirasakai\AppData\Local\Microsoft\WinGet\Packages\Gyan.FFmpeg_Microsoft.Winget.Source_8wekyb3d8bbwe\ffmpeg-8.1.2-full_build\bin\ffprobe.exe |
| curl | ARM64 | C:\Windows\System32\curl.exe |
| tar | ARM64 | C:\Windows\System32\tar.exe |
| java | x64 | C:\Program Files\Eclipse Adoptium\jdk-8.0.492.9-hotspot\bin\java.exe |
| javac | x64 | C:\Program Files\Eclipse Adoptium\jdk-8.0.492.9-hotspot\bin\javac.exe |
| code.cmd | not-PE | C:\Users\akirasakai\AppData\Local\Programs\Microsoft VS Code\bin\code.cmd |
| pwsh | ARM64 | C:\Program Files\PowerShell\7\pwsh.exe |
| powershell | ARM64 | C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe |
| docker | not-PE | C:\Program Files\Docker\Docker\resources\bin\docker |
| gh | ARM64 | C:\Program Files\GitHub CLI\gh.exe |
| winget | not-PE | C:\Users\akirasakai\AppData\Local\Microsoft\WindowsApps\winget.exe |
| wsl | ARM64 | C:\Windows\System32\wsl.exe |
| where.exe | ARM64 | C:\Windows\System32\where.exe |
| notepad | ARM64 | C:\Windows\System32\notepad.exe |
This table alone makes migration to Windows on ARM64 look quite advanced. But it describes only the entry points I use often. Usage frequency and the number of installed files are different things, and conflating them would lead to the same mistake I initially made.
The whole folders showed a different picture
By folder, only C:\Program Files was x64-heavy. LocalAppData\Programs contained 172 ARM64 files versus 30 x64 files. Per-user applications such as Microsoft Scout, GitHub Copilot, and VS Code may be more concentrated on the ARM64 side (the character of each install location is likely part of this difference).
| Folder | EXEs scanned | ARM64 | x64 | x86 |
|---|---|---|---|---|
C:\Program Files |
957 | 315 | 580 | 62 |
C:\Program Files (x86) |
154 | 99 | 3 | 52 |
C:\Users\akirasakai\AppData\Local\Programs |
225 | 172 | 30 | 23 |
By size, the ARM64 files under C:\Program Files totaled 2722562136 bytes, while x64 totaled 1365529288 bytes. x64 had more files, but ARM64 occupied more bytes. That reversal may be influenced by larger native executables such as dotnet and GitHub CLI, but I did not break it down by filename, so this remains only a hypothesis.
The ARM64 files under C:\Program Files (x86) included Google Update and Microsoft Copilot. The “x86” name is easy to overread. The Windows folder name is a compatibility convention; it does not guarantee the architecture of the binaries placed there.
I stopped once at not-PE
The small failure was how to handle not-PE entries. npm.cmd, code.cmd, docker, and winget were not PE files. Batch files, extensionless shims, and WindowsApps execution aliases were mixed in, so reading a header could not determine whether the eventual program was ARM64 or x64.
Not forcing a classification was the right choice. Typing npm or code successfully does not mean that the entry point itself is an executable, and the actual execution architecture would require checking the payload behind the wrapper. This article leaves unknown entries as not-PE (incomplete, but better than inventing a result).
I also almost misread ffmpeg. Its path was under WinGet\Packages, which could tempt me to mix it up with winget's architecture. Reading the actual ffmpeg.exe showed x64, and ffprobe was the same. That check should come before writing a video-processing benchmark.
I changed the pre-benchmark checklist
After this census, I changed the checks I do before writing a benchmark. I inspect Get-Command for the target command, read its PE header, and follow a wrapper if it is not-PE. It is tedious, but on Windows on ARM64 it is easy to mistake native execution for emulation if this step is skipped.
In my environment on June 24, 2026, the split was: frequently used command-line entry points were often ARM64, while C:\Program Files as a whole had more x64 files. A light everyday feel does not prove that everything is native. Conversely, 580 x64 files do not mean everyday work is all emulation.
The next useful test would be process-level mixing including DLLs and wrapper payloads. This method read only each EXE's Machine field, so it did not show which DLLs an application actually loaded. The boundary between ARM64 and x64 would probably look more tangled at that level.