1 March 2025 · 3 min
Jarred Sumner, the main author of the Bun JavaScript engine, commented a few days ago on X that opening many files on macOS could be slow due to thread contention: “your $5,000 computer is only capable of opening 1 file at a time”.
I was curious and I decided to test it out. I wrote a small C++ program which opens 10,000 files. The files are opened and closed immediately. However, there are 10,000 distinct (empty) files. It is a simple test: there is no writing and the files do not remain open long.
I use either 1 thread, 2 threads, 8 threads or 16 threads. The threads split the work equally: e.g., with two threads, you have two threads opening 5000 files each.
To make things interesting, let us add a “thread pool”: I create and launch all threads before the benchmark starts. Then I add all of the tasks (opening a file X) to the thread pool. This should be slower, but maybe more realistic of how engineers might solve such problems.
Indeed, with the thread pool, for each file, you need to grab a thread, assign the work, and wait for it to complete before more work can be assigned. You must grab thread for each file to be opened. When the unit of work is small (i.e., open a single file), thread pools are not very efficient. Assigning several files to be opened by a thread at once (in batches) is faster.
I run each test 10 times and I keep the median. There is some instability in the measures, I try to report representative numbers. My code is located on GitHub: I invite you to run it on your system.
Let us look first at the results on my Apple M2 laptop (it has 8 cores, 4 performance + 4 efficiency). Unfortunately, I do not have a beefier Apple computer at my disposal right now.
I report the total time.
| threads | regular | with thread pool |
|---|---|---|
| 1 thread | 100 ms | 140 ms |
| 2 threads | 75 ms | 100 ms |
| 4 threads | 90 ms | 95 ms |
| 8 threads | 240 ms | 250 ms |
| 16 threads | 250 ms | 270 ms |
Let us test it out under Linux on a big x64 server with 64 cores. Although it is a bigger machine, it has worse per-core performance than the Apple macBook in general (slower memory, slower clock, fewer instructions per cycle).
| threads | regular | with thread pool |
|---|---|---|
| 1 thread | 34 ms | 55 ms |
| 2 threads | 25 ms | 36 ms |
| 4 threads | 31 ms | 27 ms |
| 8 threads | 36 ms | 16 ms |
| 16 threads | 42 ms | 27 ms |
The macOS system has a faster disk, faster memory and faster cores. Yet opening files is clearly much slower under macOS according to this test.
I find it interesting that in both cases, using two threads minimizes the running time in the regular case: additional threads appear to slow things down.
With the big machine, a thread pool can go faster than the regular approach if I use four threads or more.
On my macBook, I cannot open much more than 120,000 files per second. My Linux server scales up to 400,000 files per second. In some cases, opening thousands of files could become a hard bottleneck. Throwing more threads at the problem might not work.
Daniel Lemire, "How fast can you open 1000 files?," in Daniel Lemire's blog, March 1, 2025, https://lemire.me/blog/2025/03/01/how-fast-can-you-open-1000-files/.
[BibTeX]
If I read correctly from your source files, your benchmark programs open 10k files, not 1k. This makes sense also in view of your comment that you split the work equally between threads, because 1k would not have been a multiple of 16.
FWIW on an M1Pro MBP (32GB/1TB),
regular: 95.58 / 56.96 / 37.50 / 79.92 ms / 104.75
threadpool: 105.47 / 62.00 / 46.85 / 77.70 / 108.97
Very interesting. Here’s mine:
M3 Pro(6 perf/6 eff/36GB mem):
open: 64.336/40.295/26.6405/44.4365/136.795
open_pool: 87.4235/55.7655/32.9095/43.728/153.274
Another Linux machine for comparison:
AMD Ryzen 7 PRO 6850U (32GB/1TB)
open: 27.04 / 15.4245 / 8.243 / 5.5465 / 5.2845 ms
open_pool: 41.5075 / 20.142 / 10.046 / 6.788 / 16.2655 ms
On linux, use stress-ng command to perform this measurements.
BTW this is just probably a MacOS kernel bug or something, it is ridiculous slow.
A Windows 11 machine with AMD Ryzen™ 9 5900X
open: 967.508 / 477.56 / 275.738 / 148.832 / 108.157 ms
open_pool: 1049.21 / 530.436 / 282.259 / 153.498 / 101.773 ms
It is slow though it scales.
Note: the makefile is slightly modified to use MSVC with /O2.