ARM64_Lab

WSL2 ext4 vs /mnt/c: DrvFs took 457x longer for small files

In this article
  1. What this WSL2 ext4 vs /mnt/c comparison actually measured
  2. 1,000 small files across six rounds
  3. Median total: 70.73 ms on ext4, 32,305.75 ms on /mnt/c
  4. Git status took 180x as long on the DrvFs path
  5. My first 5,000-file run failed after more than 20 minutes
  6. This was not an fsync or storage-durability benchmark
  7. This does not prove the gap is specific to Windows on ARM
  8. Where I now keep WSL2 projects

I created 1,000 files of 4 KB each in WSL2, read them back, ran stat on every file, and deleted them. The complete sequence took a median of 70.73 ms on ext4 inside the WSL2 VHDX and 32,305.75 ms on /mnt/c. That is a 456.75x difference on the same Surface Pro 11 with a Snapdragon X Elite X1E80100, using the same Ubuntu installation.

When I measured WSL2 aarch64 cold-start time on July 21, 2026, a short command took 15.35 seconds on the first launch. This test starts after WSL is already running. The question here is how much the location of a Linux project changes the wait time for the same file operations.

What this WSL2 ext4 vs /mnt/c comparison actually measured

The ext4 location was /tmp/arm64-lab-wsl-fs-bench. According to findmnt, it was on /dev/sdd, used ext4, and had the mount options rw,relatime,discard,errors=remount-ro,data=ordered. This was not a Linux installation writing directly to a physical SSD. It was ext4 inside the WSL2 VHDX, which itself resides on the Windows NTFS volume.

The other location was the repository's .bench/wsl-fs-work directory as seen from WSL. Its real path was on the C drive and was not inside a OneDrive-synchronized folder. WSL exposed it under /mnt/c. findmnt reported the filesystem type as 9p, the source as C:\, and mount options that included aname=drvfs and msize=65536.

I added these checks because my first rough version simply said it would compare /tmp with /mnt/c. That would be a different test if /tmp happened to be a tmpfs mount. The benchmark now inspects both filesystems at runtime and stops if they do not match the expected setup.

1,000 small files across six rounds

The benchmark script is scripts/wsl_fs_smallfiles_bench.py. ARM64 Python 3.12.10 on Windows launches wsl.exe, then reruns the same script under aarch64 Python 3.12.3 in Ubuntu.

python scripts/wsl_fs_smallfiles_bench.py --json .bench/wsl-fs-smallfiles.json

Each file was 4,096 bytes. The test created 1,000 files across 20 directories. In every round, it created the files, immediately read all of them, called stat on each one, and then deleted them. I ran one warm-up before collecting six rounds. To reduce ordering bias, ext4 ran first in three rounds and /mnt/c ran first in the other three.

Every round read 4,096,000 bytes. The sum of the sizes returned by stat was also 4,096,000 bytes, and the CRC32 was 586cb588 in every round. The benchmark also confirmed that the working directory no longer existed after deletion. A fast run did not count as successful if it silently skipped part of the data.

Median total: 70.73 ms on ext4, 32,305.75 ms on /mnt/c

The table below shows the median for each operation. Reads happened immediately after file creation, so both sides were cache-hot.

Operation ext4 inside VHDX /mnt/c 9p /mnt/c slowdown
Create 1,000 files 47.61 ms 17,183.19 ms 360.92x
Read every file immediately 10.41 ms 5,981.62 ms 574.60x
Run stat 1,000 times 2.25 ms 4,696.19 ms 2,087.20x
Delete 1,000 files 9.88 ms 5,344.51 ms 540.94x
Total, creation through deletion 70.73 ms 32,305.75 ms 456.75x

The largest relative gap appeared in stat, which transfers very little file data. It took 2.25 ms on ext4 and 4,696.19 ms on /mnt/c, a 2,087.20x difference. The absolute time was still lower than the 5,981.62 ms read phase, but querying metadata alone took about 4.7 seconds. That access pattern resembles Git and package managers checking large numbers of dependency files.

Here are all six total times.

Round ext4 total /mnt/c total
1 72.50 ms 39,006.63 ms
2 74.92 ms 45,175.41 ms
3 71.36 ms 27,170.14 ms
4 65.22 ms 31,380.33 ms
5 68.80 ms 33,004.66 ms
6 70.11 ms 31,606.83 ms

The ext4 results stayed between 65.22 ms and 74.92 ms. The /mnt/c results ranged from 27,170.14 ms to 45,175.41 ms, with the slowest round taking 1.66x as long as the fastest. In practice, I found that variability harder to work with than the headline ratio. It makes completion time less predictable, and some otherwise short operations turn into noticeable waits.

Git status took 180x as long on the DrvFs path

A synthetic create-read-stat-delete loop is not enough to decide where a real repository should live. I therefore created a Git repository with the same 1,000 tracked files in each location. I modified one file and ran git status --porcelain=v1 --untracked-files=all six times with Git 2.43.0 inside WSL. Each run had to report exactly one modification: files/d0000/f00000.bin.

Location Fastest Median Slowest
ext4 inside VHDX 4.45 ms 5.28 ms 7.15 ms
/mnt/c 9p 744.58 ms 950.97 ms 1,658.78 ms

The median difference was 180.11x. At this scale, /mnt/c still completed in about one second, so it was not unusable. However, an IDE or hook that invokes git status after every save can repeatedly add roughly 946 ms. On this machine, that was enough evidence for me to keep repositories used mainly from WSL on ext4.

My first 5,000-file run failed after more than 20 minutes

The original configuration used 5,000 files totaling 20 MB. The first ext4 round took 760.89 ms, while the first /mnt/c round took 415,538.27 ms, or about 6 minutes 56 seconds. I waited more than 20 minutes for all six rounds, and the run then stopped when it reached the Git validation step.

The failure was in my validation code, not in the filesystem. I called strip() on the output of git status --porcelain, removing its leading space, and then compared the result with a status string that still included that space. Git had returned the correct output, but my checker classified it as a failure. The script exited before writing the raw JSON, so I excluded the first 5,000-file run from the formal results.

I changed the checker to validate the status characters and file path separately. The script now also saves intermediate JSON as soon as the filesystem phase finishes. I reduced the default to 1,000 files so a rerun would take a practical amount of time. The 415.5-second result for 5,000 files remains a reference point, but it is not part of the successful six-round table.

This was not an fsync or storage-durability benchmark

The write phase measured Python's write_bytes() through file close. It did not call fsync. The creation times therefore do not mean that every byte had reached durable storage. Writeback after creation may also have overlapped with the read and stat phases that followed.

For that reason, I treat the full create-to-delete sequence as the primary result rather than presenting the four phases as isolated storage benchmarks. I wanted a pattern closer to the way Git or npm waits on many small files. The immediate reads were not cold reads, and I explicitly describe them as hot reads.

Windows Defender and real-time protection remained enabled during the benchmark. Defender had one excluded path, but this repository was not under it. I did not repeat the benchmark with Defender disabled, so the test cannot separate how much of the /mnt/c cost came from 9p, NTFS, or Defender.

This does not prove the gap is specific to Windows on ARM

The machine was a Surface Pro 11th Edition with a Snapdragon X Elite X1E80100, Windows 11 Pro 10.0.26200 ARM64, and 12 cores visible inside Linux. WSL was version 2.7.3.0, the kernel was 6.6.114.1-microsoft-standard-WSL2, and uname -m returned aarch64.

I did not run the same benchmark on an x64 Windows PC or another WSL release. The 456.75x ratio cannot be attributed specifically to Snapdragon X Elite or Windows on ARM. The measured result is limited to this ARM64 machine, comparing ext4 inside its WSL2 VHDX with the C drive accessed through the 9p/DrvFs mount.

Several relevant workloads also remain untested: cloning a real large repository, npm install, parallel builds, and directory enumeration. I did not test a workflow in which Windows and WSL tools alternately modify the same files. Keeping a project on ext4 can be less convenient for Windows applications, so these results are not an argument to move every project based on speed alone.

Where I now keep WSL2 projects

For repositories where I run Git, Node.js, and Python mainly inside WSL, I now use the Linux home directory on ext4 inside the VHDX. When I need to browse those files from Windows, I use \\wsl$\Ubuntu\home\.... If a Windows-native IDE or script is the primary tool, I leave the project on the C drive.

The surprising part was that /mnt/c was not merely a little slower. The complete 1,000-file sequence took 32.3 seconds instead of 0.071 seconds, and git status took 0.951 seconds instead of 0.005 seconds. On this machine on August 9, 2026, a WSL-first development workflow on /mnt/c waited on the filesystem boundary long before CPU performance became the limiting factor. Of the changes I measured, moving the repository was the one with the largest effect.

Benchmark WSL2 Storage Windows on ARM
a
arm64lab — Independent publisher

Personal test notes from a Surface Pro 11th Edition with Snapdragon X Elite, used as a daily machine since May 2025. Results are based on direct measurements and do not represent any company or organization.