]> Cypherpunks repositories - gostls13.git/commitdiff
os: don't fallback to the Stat slow path if file doesn't exist on Windows
authorqmuntal <quimmuntal@gmail.com>
Tue, 13 May 2025 15:26:06 +0000 (17:26 +0200)
committerQuim Muntal <quimmuntal@gmail.com>
Thu, 15 May 2025 05:22:10 +0000 (22:22 -0700)
os.Stat and os.Lstat first try stating the file without opening it. If
that fails, then they open the file and try again, operations that tends
to be slow. There is no point in trying the slow path if the file
doesn't exist, we should just return an error immediately.

This CL makes stating a non-existent file on Windows 50% faster:

goos: windows
goarch: amd64
pkg: os
cpu: Intel(R) Core(TM) i7-10850H CPU @ 2.70GHz
                │   old.txt    │                new.txt                 │
                │    sec/op    │    sec/op     vs base                  │
StatNotExist-12   43.65µ ± 15%   20.02µ ± 10%  -54.14% (p=0.000 n=10+7)

                │  old.txt   │             new.txt              │
                │    B/op    │    B/op     vs base              │
StatNotExist-12   224.0 ± 0%   224.0 ± 0%  ~ (p=1.000 n=10+7) ¹
¹ all samples are equal

                │  old.txt   │             new.txt              │
                │ allocs/op  │ allocs/op   vs base              │
StatNotExist-12   2.000 ± 0%   2.000 ± 0%  ~ (p=1.000 n=10+7) ¹

Updates #72992.

Change-Id: Iaeb9596d0d18e5a5a1bd1970e296a3480501af78
Reviewed-on: https://go-review.googlesource.com/c/go/+/671458
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Jake Bailey <jacob.b.bailey@gmail.com>
Reviewed-by: Damien Neil <dneil@google.com>
Reviewed-by: Michael Knyszek <mknyszek@google.com>
src/os/stat_test.go
src/os/stat_windows.go

index 36da573f0c02806a9192b1f717bc9bbbc2c330f6..92ecabb787a1fe12a25f252fc74dca695cbe5414 100644 (file)
@@ -361,3 +361,18 @@ func TestClosedStat(t *testing.T) {
                t.Errorf("error from Stat on closed file did not match ErrClosed: %q, type %T", err, err)
        }
 }
+
+func TestStatNotExist(t *testing.T) {
+       t.Parallel()
+       name := filepath.Join(t.TempDir(), "notfound")
+       _, err := os.Stat(name)
+       if !errors.Is(err, fs.ErrNotExist) {
+               t.Errorf("os.Stat(%q) = %v; want fs.ErrNotExist", name, err)
+       }
+
+       name = filepath.Join(t.TempDir(), "notfounddir", "notfound")
+       _, err = os.Stat(name)
+       if !errors.Is(err, fs.ErrNotExist) {
+               t.Errorf("os.Stat(%q) = %v; want fs.ErrNotExist", name, err)
+       }
+}
index d2c2017a6504048c3aa33798658acf42e2ab5e7a..e2ed58a3a0b6e9d6b2c8f296ed38655b93e20d86 100644 (file)
@@ -5,6 +5,7 @@
 package os
 
 import (
+       "errors"
        "internal/filepathlite"
        "internal/syscall/windows"
        "syscall"
@@ -34,6 +35,9 @@ func stat(funcname, name string, followSurrogates bool) (FileInfo, error) {
        // See https://golang.org/issues/19922#issuecomment-300031421 for details.
        var fa syscall.Win32FileAttributeData
        err = syscall.GetFileAttributesEx(namep, syscall.GetFileExInfoStandard, (*byte)(unsafe.Pointer(&fa)))
+       if errors.Is(err, ErrNotExist) {
+               return nil, &PathError{Op: "GetFileAttributesEx", Path: name, Err: err}
+       }
        if err == nil && fa.FileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT == 0 {
                // Not a surrogate for another named entity, because it isn't any kind of reparse point.
                // The information we got from GetFileAttributesEx is good enough for now.