Feature #21987
openAssume `chdir(2)` isn't called and cache `rb_dir_getwd_ospath()`
Description
Context¶
I'm looking at optimizing Ruby applications boot time, and one very common operation that seems wasteful is that when interpreting relative paths
we keep calling ruby_getcwd(), even though its result almost never changes (Dir.chdir calls are rare).
We have 3 distinct implementations of ruby_getcwd() depending on the platform, but generally speaking it involves:
- One or multiple syscalls (on macOS:
open+fnctl+stat+close). -
malloc+free -
strdup+free - For some implementations, allocating a
T_DATAto avoid leaks in case of failure.
Measure¶
Out of curiosity I measured our monolith. Just booting we end up calling getcwd(2) 6931 times, for a total of 90ms spent in rb_dir_getwd_ospath(), but the caching would allow to save a bit more than that, as callers would no longer need to manage that memory.
Proposal¶
I propose that we cache the result of ruby_getcwd(), and only flush the cache when Dir.chdir is called.
Compatibility concern¶
This however assumes that C extensions or other native libraries never call chdir(2) after executing Ruby, or at least always reset it before yielding back to Ruby.
I'm not aware of any C extension currently doing this, but it would be interesting to perform a gem-codesearch.
If necessary we can provide void ruby_cwd_changed(void) to reset the cache after calling chdir(2).
Updated by nobu (Nobuyoshi Nakada) 1 day ago
byroot (Jean Boussier) wrote:
This however assumes that C extensions or other native libraries never call
chdir(2)after executing Ruby, or at least always reset it before yielding back to Ruby.
And no other process renames the directory?
Updated by byroot (Jean Boussier) 1 day ago
Hum, that's a case i didn't think of indeed... Might be a deal breaker.
Updated by jhawthorn (John Hawthorn) 1 day ago
byroot (Jean Boussier) wrote:
For some implementations, allocating a
T_DATAto avoid leaks in case of failure.
Is this still the case? I removed some of this in https://github.com/ruby/ruby/commit/126b657bd103a1abf4b572ade557ffdc7ae99982, which improved by about 30% on Linux.
I had meant to go further like optimistically reading the syscall into a stack buffer instead of using malloc/xmalloc, which it looks like you're already investigating :)
Updated by byroot (Jean Boussier) 1 day ago
ยท Edited
Is this still the case? I removed some of this in
My bad. My first prototype of that change was months ago, this is indeed no longer the case.
which it looks like you're already investigating :)
Yes, that was my backup plan from the start. The problem is that on macOS, open(2) is incredibly slow.
The performance difference of getcwd on Linux vs macOS is about 40x:
compare-ruby: ruby 4.1.0dev (2026-04-09T05:19:02Z master c091c186e4) +PRISM [arm64-darwin25]
built-ruby: ruby 4.1.0dev (2026-04-09T06:37:20Z get-cwd-cache ea02126d79) +PRISM [arm64-darwin25]
| compare-ruby | built-ruby | |
|---|---|---|
| Dir.pwd | 105.183k | 113.420k |
| - | 1.08x |
Linux (inside virtualized Docker)
compare-ruby: ruby 4.1.0dev (2026-04-07T08:26:25Z master fcd210086c) +PRISM [aarch64-linux]
built-ruby: ruby 4.1.0dev (2026-04-09T06:38:09Z get-cwd-cache 6774af9ba7) +PRISM [aarch64-linux]
| compare-ruby | built-ruby | |
|---|---|---|
| Dir.pwd | 4.157M | 5.541M |
| - | 1.33x |
But if we can't cache getcwd, then I'll try to figure out why it's called so much, and eliminate that.