From 6144c7270e5812d9de8fb97456ee4e5ae657fcbb Mon Sep 17 00:00:00 2001 From: Alex Brainman Date: Tue, 9 May 2017 16:50:41 +1000 Subject: [PATCH] os: make windows Stat as fast as Lstat for files and directories MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Recent CL 41834 made windows Stat work for all symlinks. But CL 41834 also made Stat slow. John Starks sugested (see https://github.com/golang/go/issues/19922#issuecomment-300031421) to use GetFileAttributesEx for files and directories instead. This makes Stat as fast as at go1.9. I see these improvements on my Windows 7 name old time/op new time/op delta StatDot 26.5µs ± 1% 20.6µs ± 2% -22.37% (p=0.000 n=9+10) StatFile 22.8µs ± 2% 6.2µs ± 1% -72.69% (p=0.000 n=10+10) StatDir 21.0µs ± 2% 6.1µs ± 3% -71.12% (p=0.000 n=10+9) LstatDot 20.1µs ± 1% 20.7µs ± 6% +3.37% (p=0.000 n=9+10) LstatFile 6.23µs ± 1% 6.36µs ± 8% ~ (p=0.587 n=9+10) LstatDir 6.10µs ± 0% 6.14µs ± 4% ~ (p=0.590 n=9+10) and on my Windows XP name old time/op new time/op delta StatDot-2 20.6µs ± 0% 10.8µs ± 0% -47.44% (p=0.000 n=10+10) StatFile-2 20.2µs ± 0% 7.9µs ± 0% -60.91% (p=0.000 n=8+10) StatDir-2 19.3µs ± 0% 7.6µs ± 0% -60.51% (p=0.000 n=10+9) LstatDot-2 10.8µs ± 0% 10.8µs ± 0% -0.48% (p=0.000 n=10+8) LstatFile-2 7.83µs ± 0% 7.83µs ± 0% ~ (p=0.844 n=10+8) LstatDir-2 7.59µs ± 0% 7.56µs ± 0% -0.46% (p=0.000 n=10+10) Updates #19922 Change-Id: Ice1fb5825defb05c79bab4dec0692e0fd1bcfcd5 Reviewed-on: https://go-review.googlesource.com/43071 Reviewed-by: Austin Clements Run-TryBot: Alex Brainman TryBot-Result: Gobot Gobot --- src/os/os_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++ src/os/stat_windows.go | 20 +++++++++++++++++-- 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/src/os/os_test.go b/src/os/os_test.go index 22777aef9f..91c6be6148 100644 --- a/src/os/os_test.go +++ b/src/os/os_test.go @@ -398,6 +398,50 @@ func BenchmarkReaddir(b *testing.B) { benchmarkReaddir(".", b) } +func benchmarkStat(b *testing.B, path string) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := Stat(path) + if err != nil { + b.Fatalf("Stat(%q) failed: %v", path, err) + } + } +} + +func benchmarkLstat(b *testing.B, path string) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := Lstat(path) + if err != nil { + b.Fatalf("Lstat(%q) failed: %v", path, err) + } + } +} + +func BenchmarkStatDot(b *testing.B) { + benchmarkStat(b, ".") +} + +func BenchmarkStatFile(b *testing.B) { + benchmarkStat(b, filepath.Join(runtime.GOROOT(), "src/os/os_test.go")) +} + +func BenchmarkStatDir(b *testing.B) { + benchmarkStat(b, filepath.Join(runtime.GOROOT(), "src/os")) +} + +func BenchmarkLstatDot(b *testing.B) { + benchmarkLstat(b, ".") +} + +func BenchmarkLstatFile(b *testing.B) { + benchmarkLstat(b, filepath.Join(runtime.GOROOT(), "src/os/os_test.go")) +} + +func BenchmarkLstatDir(b *testing.B) { + benchmarkLstat(b, filepath.Join(runtime.GOROOT(), "src/os")) +} + // Read the directory one entry at a time. func smallReaddirnames(file *File, length int, t *testing.T) []string { names := make([]string, length) diff --git a/src/os/stat_windows.go b/src/os/stat_windows.go index a7220041cd..667b99905d 100644 --- a/src/os/stat_windows.go +++ b/src/os/stat_windows.go @@ -71,7 +71,23 @@ func Stat(name string) (FileInfo, error) { if err != nil { return nil, &PathError{"Stat", name, err} } - + // Apparently (see https://github.com/golang/go/issues/19922#issuecomment-300031421) + // GetFileAttributesEx is fastest approach to get file info. + // It does not work for symlinks. But symlinks are rare, + // so try GetFileAttributesEx first. + var fs fileStat + err = syscall.GetFileAttributesEx(namep, syscall.GetFileExInfoStandard, (*byte)(unsafe.Pointer(&fs.sys))) + if err == nil && fs.sys.FileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT == 0 { + fs.path = name + if !isAbs(fs.path) { + fs.path, err = syscall.FullPath(fs.path) + if err != nil { + return nil, &PathError{"FullPath", name, err} + } + } + fs.name = basename(name) + return &fs, nil + } // Use Windows I/O manager to dereference the symbolic link, as per // https://blogs.msdn.microsoft.com/oldnewthing/20100212-00/?p=14963/ h, err := syscall.CreateFile(namep, 0, 0, nil, @@ -170,7 +186,7 @@ func Lstat(name string) (FileInfo, error) { if !isAbs(fs.path) { fs.path, e = syscall.FullPath(fs.path) if e != nil { - return nil, e + return nil, &PathError{"FullPath", name, e} } } return fs, nil -- 2.50.0