找回密码
 立即注册

QQ登录

只需一步,快速开始

Go1.16引入目次遍历优化解析

2024-11-4 20:49| 发布者: 4d5a8576d| 查看: 216| 评论: 0

摘要: 一转眼go1.23都快发布了,时间过得真快。 不过今天我们把时间倒流回三年半之前,来关注一个在go1.16引入的关于处理处罚目次时的优化。 对于go1.16的新变化,大家印象最深的可能是io包的大规模重构,但这个重构实际上

一转眼go1.23都快发布了,时间过得真快。

不过今天我们把时间倒流回三年半之前,来关注一个在go1.16引入的关于处理处罚目次时的优化。

对于go1.16的新变化,大家印象最深的可能是io包的大规模重构,但这个重构实际上还引进了一个优化,这篇文章要说的就是这个优化。

本文默认Linux环境,不过这个优化在BSD体系上也是通用的。

遍历目次时的优化

遍历目次是个很常见的需求,尤其是对于有大量文件的目次来说,遍历的性能直接关系到了团体程序的性能。

go1.16对于遍历目次增加了几个新接口:os.ReadDir,(*os.File).ReadDir,filepath.WalkDir。

这几个接口最大的特性是对目次项利用fs.DirEntry表示而不是os.FileInfo。fs.DirEntry是一个接口,它提供了类似os.FileInfo的方法:

[code]type DirEntry interface { Name() string IsDir() bool Type() FileMode Info() (FileInfo, error) }[/code]

它还提供了一个叫Info的方法以便得到os.FileInfo。

这个接口有什么神奇的呢?我们看下性能测试:

[code]func IterateDir(path string) int { // go1.16 的 os.ReadDir 就是这么实现的,为了测试我们把它展开成对(*os.File).ReadDir的调用 f, err := os.Open(path) if err != nil { panic(err) } defer f.Close() files, err := f.ReadDir(-1) if err != nil { panic(err) } length := 0 for _, finfo := range files { length = max(length, len(finfo.Name())) } return length } func IterateDir2(path string) int { // 1.16之前遍历目次的常用方法之一 f, err := os.Open(path) if err != nil { panic(err) } defer f.Close() files, err := f.Readdir(-1) if err != nil { panic(err) } length := 0 for _, finfo := range files { length = max(length, len(finfo.Name())) } return length } func BenchmarkIter1(b *testing.B) { for range b.N { IterateDir("../test") } } func BenchmarkIter2(b *testing.B) { for range b.N { IterateDir2("../test") } }[/code]

test目次是一个有5000个文件的位于Btrfs文件体系上的目次,我们的测试用例会遍历目次并找着名字最长的文件的文件名长度。

这是测试结果:

可以看到优化后的遍历比原先的快了480%。换了个函数为什么就会有这么大的提拔?想知道答案的话就继承看吧。

优化的原理

继承深入前我们先看看老的接口是怎么获取到目次里的文件信息的。答案是遍历目次拿到路径,然后调用os.Lstat获取完备的文件信息:

[code]func (f *File) Readdir(n int) ([]FileInfo, error) { if f == nil { return nil, ErrInvalid } _, _, infos, err := f.readdir(n, readdirFileInfo) if infos == nil { // Readdir has historically always returned a non-nil empty slice, never nil, // even on error (except misuse with nil receiver above). // Keep it that way to avoid breaking overly sensitive callers. infos = []FileInfo{} } return infos, err }[/code]

这个f.readdir会根据第二个参数的值来改变自己的行为,根据值不同它会遵循1.16前老代码的行为或者采用新的优化方法。这个函数不同体系上的实现也不同,我们选则*nix体系上的实现看看:

[code]func (f *File) readdir(n int, mode readdirMode) (names []string, dirents []DirEntry, infos []FileInfo, err error) { ... for n != 0 { // 利用体系调用得到目次项的数据 // 目次项的元信息一样平常是存储在目次本身的数据里的,所以读这些信息和读平凡文件很类似 if d.bufp >= d.nbuf { d.bufp = 0 var errno error d.nbuf, errno = f.pfd.ReadDirent(*d.buf) runtime.KeepAlive(f) if errno != nil { return names, dirents, infos, &PathError{Op: "readdirent", Path: f.name, Err: errno} } if d.nbuf <= 0 { break // EOF } } buf := (*d.buf)[d.bufp:d.nbuf] reclen, ok := direntReclen(buf) if !ok || reclen > uint64(len(buf)) { break } // 注意这行 rec := buf[:reclen] if mode == readdirName { names = append(names, string(name)) } else if mode == readdirDirEntry { // 这里的代码背面再看 } else { info, err := lstat(f.name + "/" + string(name)) if IsNotExist(err) { // File disappeared between readdir + stat. // Treat as if it didn't exist. continue } if err != nil { return nil, nil, infos, err } infos = append(infos, info) } } if n > 0 && len(names)+len(dirents)+len(infos) == 0 { return nil, nil, nil, io.EOF } return names, dirents, infos, nil }[/code]

ReadDirent对应的是Linux上的体系调用getdents,这个体系调用会把目次的目次项信息读取到一块内存里,之后程序可以解析这块内存里的数据来得到目次项的一些信息,这些信息一样平常包括了文件名,文件的类型,文件是否是目次等信息。

老代码在读取完这些信息后会利用文件名再次调用lstat,这个也是体系调用,可以获取更完备的文件信息,包括了文件的拥有者,文件的大小,文件的修改日期等。

老的代码有啥题目呢?大的题目不存在,接口也算易用,但有些小瑕疵:

大多数时间遍历目次重要是要得到目次中文件的名字或者类型等属性,显然os.FileInfo返回的信息过多了。这些用不着的信息会浪费不少内存,获取这些信息也必要额外花时间——lstat必要去进行磁盘io才能得到这些信息,而目次里的文件不像目次项信息那样紧密的存储在一起,它们是分散的,所以一一读取它们的元信息带来的负担会很大。 利用的体系调用太多了。由于我们测试目次的文件许多,但getdents可能要调用多次,这里假设为两次好了。对于每一个目次项,都必要用lstat去获取文件的详细信息,这样又有5000次体系调用,加起来是5002次。体系调用的开销是很大的,积聚到5000多次则会带来肉眼可见的性能降落。实际上linux本身对lstat有优化,不会真的出现要反复进入体系调用5000次的环境,但几十到上百次照旧必要的。

优化的代码其实只改了一行,是f.readdir(n, readdirDirEntry),第二个参数变了。新代码会走上面表明掉的那段逻辑:

[code]// rec := buf[:reclen] 防止你忘了rec是哪来的 de, err := newUnixDirent(f.name, string(name), direntType(rec)) if IsNotExist(err) { // File disappeared between readdir and stat. // Treat as if it didn't exist. continue } if err != nil { return nil, dirents, nil, err } dirents = append(dirents, de)[/code]

代替lstat的是函数newUnixDirent,这个函数可以不依靠额外的体系调用获取文件的一部门元数据:

[code]type unixDirent struct { parent string name string typ FileMode info FileInfo } func newUnixDirent(parent, name string, typ FileMode) (DirEntry, error) { ude := &unixDirent{ parent: parent, name: name, typ: typ, } // 检测文件类型信息是否有用 if typ != ^FileMode(0) && !testingForceReadDirLstat { return ude, nil } info, err := lstat(parent + "/" + name) if err != nil { return nil, err } ude.typ = info.Mode().Type() ude.info = info return ude, nil }[/code]

文件名和类型都是在解析目次项时就得到的,因此直接设置就行。不过不是每个文件体系都支持在目次项数据里存储文件类型,所以代码里做了回退,一旦发现文件类型是无效数据就会利用lstat重新获取信息。

如果只利用文件名和文件的类型这两个信息,那么整个遍历的逻辑流程到这就结束了,文件体系提供支持的环境下不必要调用lstat。所以整个遍历只必要两次体系调用。这就是为什么优化方案会快接近五倍的缘故起因。

对于要利用其他信息比如文件大小的用户,优化方案实际上也有好处,由于现在lstat是延迟且按需调用的:

[code]func (d *unixDirent) Info() (FileInfo, error) { if d.info != nil { return d.info, nil } // 只会调用一次 return lstat(d.parent + "/" + d.name) }[/code]

这样也能尽量淘汰不必要的体系调用。

所以团体优化的原理是:尽量充分利用文件体系本身提供的信息+淘汰体系调用。要遍历的目次越大优化的效果也越显着。

优化的支持环境

上面也说了,能做到优化必要文件体系把文件类型信息存储在目次的目次项数据里。这个必要文件体系的支持。

如果文件体系不支持的话末了照旧必要依靠lstat去读取详细文件的元数据。

不同文件体系的信息着实太分散,另有不少过时的,所以我花了几天看代码+查文档做了下整理:

  • btrfs,ext2,ext4:这个几个文件体系支持优化,man pages加文件体系代码都能证明这一点
  • OpenZFS:这个文件体系不在Linux内核里,所以man pages里没提到,但也支持优化
  • xfs:支持优化,但得在创建文件体系时利用类似[code]mkfs.xfs -f -n ftype=1[/code]的选项才行
  • F2FS,EROFS:文档没提过,但看内核的代码里是支持的,代码的位置在[code]xxx_readdir[/code]这个函数附近。
  • fat32,exfat:文档没提过,但看内核代码发现是支持的,不过fat家族的文件类型没有那么多格式,只有目次和平凡文件这两种,所以代码里很粗暴的判断目次项是否设置了dir标志,有就是目次没有统统算平凡文件。这么做倒是正常的,由于fat原来就不支持别的文件类型,毕竟这个文件体系连软链接都不支持,更不用指望Unix Domain Socket和命名管道了。
  • ntfs:支持,然而如表明所说,由于ntfs和其他文件体系处理处罚type的方式不一样,导致虽然文件体系本身支持大部门文件类型,但type信息里只能得到文件是不是目次。所以它背面临于不是目次的文件会去磁盘上读取文件的inode然后再从inode里获取文件类型——实际上相称于实行了一次lstat,相比lstat淘汰了进入体系调用时的一次上下文切换,所以ntfs上优化效果会不如其他文件体系。

这么一看的话基本上主流的常见的文件体系都支持这种优化。

这也是为什么go1.16会引入这个优化,不仅支持广泛而且提拔很大,免费的加速谁不爱呢。

别的语言里怎么利用这个优化

看到这里,你应该发现这个优化其实是体系层面的,golang只不过是适配了一下而已。

确实是这样的,所以这个优化不光golang能吃到,c/c++/python都行。

先说说c里怎么利用:直接用体系提供的readdir函数就行,这个函数会调用getdents,然后就能自然吃到优化了。注意事项和go的一样,必要检测文件体系是否支持设置d_type。

c++:和c一样,别的libstdc++的filesystem就是拿readdir实现的,所以用filesystem标准库也能得到优化:

[code]// https://github.com/gcc-mirror/gcc/blob/master/libstdc++-v3/src/filesystem/dir-common.h#L270 inline file_type get_file_type(const std::filesystem::__gnu_posix::dirent& d [[gnu::unused]]) { #ifdef _GLIBCXX_HAVE_STRUCT_DIRENT_D_TYPE switch (d.d_type) { case DT_BLK: return file_type::block; case DT_CHR: return file_type::character; case DT_DIR: return file_type::directory; case DT_FIFO: return file_type::fifo; case DT_LNK: return file_type::symlink; case DT_REG: return file_type::regular; case DT_SOCK: return file_type::socket; case DT_UNKNOWN: return file_type::unknown; default: return file_type::none; } #else return file_type::none; #endif } // 如果操作体系以及文件体系不支持,则回退到lstat // https://github.com/gcc-mirror/gcc/blob/master/libstdc++-v3/include/bits/fs_dir.h#L342 file_type _M_file_type() const { if (_M_type != file_type::none && _M_type != file_type::symlink) return _M_type; return status().type(); }[/code]

唯一的区别在于如果目标文件是软毗连,也会调用stat。

python:利用os.scandir可以得到优化,底层和c一样利用readdir:https://github.com/python/cpython/blob/main/Modules/posixmodule.c#L16211,实现方法甚至类名都和golang很像,代码就不贴了。

总结

go虽然性能上不停被诟病,但在体系编程上倒是不暗昧,基本常见的优化都有做,可以经常关注下新版本的release notes去看看go在这方面做的积极。

看着简朴的优化,背后的可行性验证确实很复杂的,尤其是不同文件体系在怎么存储额外的元数据上很不相同,光是看代码就花了不少时间。

前面提到的ntfs在优化效果上会办理扣头,所以我特意拿Windows设备测试了下,测试条件稳定:

可以看到险些没什么区别。如果不是看了linux的ntfs驱动,我是不知道会产生这样的结果的。所以这个优化Windows上效果不抱负,但在Linux和MacOS上是实用的。

大胆假设,小心求证,体系编程和性能优化的乐趣也正在于此。

参考

exfat的fuse驱动填充d_type的逻辑:https://github.com/relan/exfat/blob/master/libexfat/utils.c

Linux的ntfs驱动必要获取文件的inode才能得到正确的file type:https://github.com/torvalds/linux/blob/master/fs/ntfs3/dir.c

到此这篇关于Go1.16引入目次遍历优化解析的文章就先容到这了,更多相关目次遍历优化解析内容请搜索脚本之家以前的文章或继承浏览下面的相关文章渴望大家以后多多支持脚本之家!


来源:https://www.jb51.net/python/3282617xm.htm
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

最新评论

关闭

站长推荐上一条 /6 下一条

QQ|手机版|小黑屋|梦想之都-俊月星空 ( 粤ICP备18056059号 )|网站地图

GMT+8, 2025-7-1 19:10 , Processed in 0.037338 second(s), 20 queries .

Powered by Mxzdjyxk! X3.5

© 2001-2025 Discuz! Team.

返回顶部