golang os *File.Readdir 在所有文件上使用 lstat。可以优化吗?
golang os *File.Readdir using lstat on all files. Can it be optimised?
我正在编写一个程序,该程序使用 os.File.Readdir
从包含大量文件的父目录中查找所有子目录,但是 运行 和 strace
来查看计数系统调用显示 go 版本在父目录中存在的所有 files/directories 上使用 lstat()
。 (我目前正在使用 /usr/bin
目录进行测试)
转到代码:
package main
import (
"fmt"
"os"
)
func main() {
x, err := os.Open("/usr/bin")
if err != nil {
panic(err)
}
y, err := x.Readdir(0)
if err != nil {
panic(err)
}
for _, i := range y {
fmt.Println(i)
}
}
程序上的 Strace(没有关注线程):
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
93.62 0.004110 2 2466 write
3.46 0.000152 7 22 getdents64
2.92 0.000128 0 2466 lstat // this increases with increase in no. of files.
0.00 0.000000 0 11 mmap
0.00 0.000000 0 1 munmap
0.00 0.000000 0 114 rt_sigaction
0.00 0.000000 0 8 rt_sigprocmask
0.00 0.000000 0 1 sched_yield
0.00 0.000000 0 3 clone
0.00 0.000000 0 1 execve
0.00 0.000000 0 2 sigaltstack
0.00 0.000000 0 1 arch_prctl
0.00 0.000000 0 1 gettid
0.00 0.000000 0 57 futex
0.00 0.000000 0 1 sched_getaffinity
0.00 0.000000 0 1 openat
------ ----------- ----------- --------- --------- ----------------
100.00 0.004390 5156 total
我用 C 的 readdir()
进行了相同的测试,但没有看到这种行为。
C代码:
#include <stdio.h>
#include <dirent.h>
int main (void) {
DIR* dir_p;
struct dirent* dir_ent;
dir_p = opendir ("/usr/bin");
if (dir_p != NULL) {
// The readdir() function returns a pointer to a dirent structure representing the next
// directory entry in the directory stream pointed to by dirp.
// It returns NULL on reaching the end of the directory stream or if an error occurred.
while ((dir_ent = readdir (dir_p)) != NULL) {
// printf("%s", dir_ent->d_name);
// printf("%d", dir_ent->d_type);
if (dir_ent->d_type == DT_DIR) {
printf("%s is a directory", dir_ent->d_name);
} else {
printf("%s is not a directory", dir_ent->d_name);
}
printf("\n");
}
(void) closedir(dir_p);
}
else
perror ("Couldn't open the directory");
return 0;
}
程序上的 Strace:
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
100.00 0.000128 0 2468 write
0.00 0.000000 0 1 read
0.00 0.000000 0 3 open
0.00 0.000000 0 3 close
0.00 0.000000 0 4 fstat
0.00 0.000000 0 8 mmap
0.00 0.000000 0 3 mprotect
0.00 0.000000 0 1 munmap
0.00 0.000000 0 3 brk
0.00 0.000000 0 3 3 access
0.00 0.000000 0 1 execve
0.00 0.000000 0 4 getdents
0.00 0.000000 0 1 arch_prctl
------ ----------- ----------- --------- --------- ----------------
100.00 0.000128 2503 3 total
我知道 POSIX.1 要求的 dirent 结构中的唯一字段是 d_name 和 d_ino,但我是为特定的文件系统编写的。
尝试了 *File.Readdirnames()
,它不使用 lstat
并给出了所有文件和目录的列表,但要查看返回的字符串是文件还是目录,最终会做一个再次lstat
。
- 我想知道是否可以重写 go 程序以避免不必要地在所有文件上使用
lstat()
。我可以看到 C 程序正在使用以下系统调用。 open("/usr/bin", O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFDIR|0755, st_size=69632, ...}) = 0
brk(NULL) = 0x1098000
brk(0x10c1000) = 0x10c1000
getdents(3, /* 986 entries */, 32768) = 32752
- 这是不是过早的优化,我不应该担心?我提出这个问题是因为被监控的目录中的文件数量将有大量的小归档文件,并且系统调用的差异在
C
和 GO
版本之间几乎是两倍,这将被击中磁盘。
包 dirent
看起来可以满足您的需求。以下是您用 Go 编写的 C 示例:
package main
import (
"bytes"
"fmt"
"io"
"github.com/EricLagergren/go-gnulib/dirent"
"golang.org/x/sys/unix"
)
func int8ToString(s []int8) string {
var buff bytes.Buffer
for _, chr := range s {
if chr == 0x00 {
break
}
buff.WriteByte(byte(chr))
}
return buff.String()
}
func main() {
stream, err := dirent.Open("/usr/bin")
if err != nil {
panic(err)
}
defer stream.Close()
for {
entry, err := stream.Read()
if err != nil {
if err == io.EOF {
break
}
panic(err)
}
name := int8ToString(entry.Name[:])
if entry.Type == unix.DT_DIR {
fmt.Printf("%s is a directory\n", name)
} else {
fmt.Printf("%s is not a directory\n", name)
}
}
}
从 Go 1.16(2021 年 2 月)开始,一个不错的选择是 os.ReadDir
:
package main
import "os"
func main() {
files, e := os.ReadDir(".")
if e != nil {
panic(e)
}
for _, file := range files {
println(file.Name())
}
}
os.ReadDir
returns fs.DirEntry
而不是 fs.FileInfo
,这意味着
Size
和 ModTime
方法被省略,使过程更有效率。
我正在编写一个程序,该程序使用 os.File.Readdir
从包含大量文件的父目录中查找所有子目录,但是 运行 和 strace
来查看计数系统调用显示 go 版本在父目录中存在的所有 files/directories 上使用 lstat()
。 (我目前正在使用 /usr/bin
目录进行测试)
转到代码:
package main
import (
"fmt"
"os"
)
func main() {
x, err := os.Open("/usr/bin")
if err != nil {
panic(err)
}
y, err := x.Readdir(0)
if err != nil {
panic(err)
}
for _, i := range y {
fmt.Println(i)
}
}
程序上的 Strace(没有关注线程):
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
93.62 0.004110 2 2466 write
3.46 0.000152 7 22 getdents64
2.92 0.000128 0 2466 lstat // this increases with increase in no. of files.
0.00 0.000000 0 11 mmap
0.00 0.000000 0 1 munmap
0.00 0.000000 0 114 rt_sigaction
0.00 0.000000 0 8 rt_sigprocmask
0.00 0.000000 0 1 sched_yield
0.00 0.000000 0 3 clone
0.00 0.000000 0 1 execve
0.00 0.000000 0 2 sigaltstack
0.00 0.000000 0 1 arch_prctl
0.00 0.000000 0 1 gettid
0.00 0.000000 0 57 futex
0.00 0.000000 0 1 sched_getaffinity
0.00 0.000000 0 1 openat
------ ----------- ----------- --------- --------- ----------------
100.00 0.004390 5156 total
我用 C 的 readdir()
进行了相同的测试,但没有看到这种行为。
C代码:
#include <stdio.h>
#include <dirent.h>
int main (void) {
DIR* dir_p;
struct dirent* dir_ent;
dir_p = opendir ("/usr/bin");
if (dir_p != NULL) {
// The readdir() function returns a pointer to a dirent structure representing the next
// directory entry in the directory stream pointed to by dirp.
// It returns NULL on reaching the end of the directory stream or if an error occurred.
while ((dir_ent = readdir (dir_p)) != NULL) {
// printf("%s", dir_ent->d_name);
// printf("%d", dir_ent->d_type);
if (dir_ent->d_type == DT_DIR) {
printf("%s is a directory", dir_ent->d_name);
} else {
printf("%s is not a directory", dir_ent->d_name);
}
printf("\n");
}
(void) closedir(dir_p);
}
else
perror ("Couldn't open the directory");
return 0;
}
程序上的 Strace:
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
100.00 0.000128 0 2468 write
0.00 0.000000 0 1 read
0.00 0.000000 0 3 open
0.00 0.000000 0 3 close
0.00 0.000000 0 4 fstat
0.00 0.000000 0 8 mmap
0.00 0.000000 0 3 mprotect
0.00 0.000000 0 1 munmap
0.00 0.000000 0 3 brk
0.00 0.000000 0 3 3 access
0.00 0.000000 0 1 execve
0.00 0.000000 0 4 getdents
0.00 0.000000 0 1 arch_prctl
------ ----------- ----------- --------- --------- ----------------
100.00 0.000128 2503 3 total
我知道 POSIX.1 要求的 dirent 结构中的唯一字段是 d_name 和 d_ino,但我是为特定的文件系统编写的。
尝试了 *File.Readdirnames()
,它不使用 lstat
并给出了所有文件和目录的列表,但要查看返回的字符串是文件还是目录,最终会做一个再次lstat
。
- 我想知道是否可以重写 go 程序以避免不必要地在所有文件上使用
lstat()
。我可以看到 C 程序正在使用以下系统调用。open("/usr/bin", O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC) = 3 fstat(3, {st_mode=S_IFDIR|0755, st_size=69632, ...}) = 0 brk(NULL) = 0x1098000 brk(0x10c1000) = 0x10c1000 getdents(3, /* 986 entries */, 32768) = 32752
- 这是不是过早的优化,我不应该担心?我提出这个问题是因为被监控的目录中的文件数量将有大量的小归档文件,并且系统调用的差异在
C
和GO
版本之间几乎是两倍,这将被击中磁盘。
包 dirent
看起来可以满足您的需求。以下是您用 Go 编写的 C 示例:
package main
import (
"bytes"
"fmt"
"io"
"github.com/EricLagergren/go-gnulib/dirent"
"golang.org/x/sys/unix"
)
func int8ToString(s []int8) string {
var buff bytes.Buffer
for _, chr := range s {
if chr == 0x00 {
break
}
buff.WriteByte(byte(chr))
}
return buff.String()
}
func main() {
stream, err := dirent.Open("/usr/bin")
if err != nil {
panic(err)
}
defer stream.Close()
for {
entry, err := stream.Read()
if err != nil {
if err == io.EOF {
break
}
panic(err)
}
name := int8ToString(entry.Name[:])
if entry.Type == unix.DT_DIR {
fmt.Printf("%s is a directory\n", name)
} else {
fmt.Printf("%s is not a directory\n", name)
}
}
}
从 Go 1.16(2021 年 2 月)开始,一个不错的选择是 os.ReadDir
:
package main
import "os"
func main() {
files, e := os.ReadDir(".")
if e != nil {
panic(e)
}
for _, file := range files {
println(file.Name())
}
}
os.ReadDir
returns fs.DirEntry
而不是 fs.FileInfo
,这意味着
Size
和 ModTime
方法被省略,使过程更有效率。