Go1.16引入目錄遍歷優(yōu)化解析
一轉(zhuǎn)眼go1.23都快發(fā)布了,時(shí)間過得真快。
不過今天我們把時(shí)間倒流回三年半之前,來關(guān)注一個(gè)在go1.16引入的關(guān)于處理目錄時(shí)的優(yōu)化。
對于go1.16的新變化,大家印象最深的可能是io包的大規(guī)模重構(gòu),但這個(gè)重構(gòu)實(shí)際上還引進(jìn)了一個(gè)優(yōu)化,這篇文章要說的就是這個(gè)優(yōu)化。
本文默認(rèn)Linux環(huán)境,不過這個(gè)優(yōu)化在BSD系統(tǒng)上也是通用的。
遍歷目錄時(shí)的優(yōu)化
遍歷目錄是個(gè)很常見的需求,尤其是對于有大量文件的目錄來說,遍歷的性能直接關(guān)系到了整體程序的性能。
go1.16對于遍歷目錄增加了幾個(gè)新接口:os.ReadDir,(*os.File).ReadDir,filepath.WalkDir。
這幾個(gè)接口最大的特征是對目錄項(xiàng)使用fs.DirEntry表示而不是os.FileInfo。fs.DirEntry是一個(gè)接口,它提供了類似os.FileInfo的方法:
type DirEntry interface { Name() string IsDir() bool Type() FileMode Info() (FileInfo, error) }
它還提供了一個(gè)叫Info的方法以便獲得os.FileInfo。
這個(gè)接口有什么神奇的呢?我們看下性能測試:
func IterateDir(path string) int { // go1.16 的 os.ReadDir 就是這么實(shí)現(xiàn)的,為了測試我們把它展開成對(*os.File).ReadDir的調(diào)用 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") } }
test目錄是一個(gè)有5000個(gè)文件的位于Btrfs文件系統(tǒng)上的目錄,我們的測試用例會(huì)遍歷目錄并找出名字最長的文件的文件名長度。
這是測試結(jié)果:
可以看到優(yōu)化后的遍歷比原先的快了480%。換了個(gè)函數(shù)為什么就會(huì)有這么大的提升?想知道答案的話就繼續(xù)看吧。
優(yōu)化的原理
繼續(xù)深入前我們先看看老的接口是怎么獲取到目錄里的文件信息的。答案是遍歷目錄拿到路徑,然后調(diào)用os.Lstat獲取完整的文件信息:
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 }
這個(gè)f.readdir會(huì)根據(jù)第二個(gè)參數(shù)的值來改變自己的行為,根據(jù)值不同它會(huì)遵循1.16前老代碼的行為或者采用新的優(yōu)化方法。這個(gè)函數(shù)不同系統(tǒng)上的實(shí)現(xiàn)也不同,我們選則*nix系統(tǒng)上的實(shí)現(xiàn)看看:
func (f *File) readdir(n int, mode readdirMode) (names []string, dirents []DirEntry, infos []FileInfo, err error) { ... for n != 0 { // 使用系統(tǒng)調(diào)用獲得目錄項(xiàng)的數(shù)據(jù) // 目錄項(xiàng)的元信息一般是存儲(chǔ)在目錄本身的數(shù)據(jù)里的,所以讀這些信息和讀普通文件很類似 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 }
ReadDirent對應(yīng)的是Linux上的系統(tǒng)調(diào)用getdents,這個(gè)系統(tǒng)調(diào)用會(huì)把目錄的目錄項(xiàng)信息讀取到一塊內(nèi)存里,之后程序可以解析這塊內(nèi)存里的數(shù)據(jù)來獲得目錄項(xiàng)的一些信息,這些信息一般包括了文件名,文件的類型,文件是否是目錄等信息。
老代碼在讀取完這些信息后會(huì)利用文件名再次調(diào)用lstat,這個(gè)也是系統(tǒng)調(diào)用,可以獲取更完整的文件信息,包括了文件的擁有者,文件的大小,文件的修改日期等。
老的代碼有啥問題呢?大的問題不存在,接口也算易用,但有些小瑕疵:
大多數(shù)時(shí)間遍歷目錄主要是要獲得目錄中文件的名字或者類型等屬性,顯然os.FileInfo返回的信息過多了。這些用不著的信息會(huì)浪費(fèi)不少內(nèi)存,獲取這些信息也需要額外花時(shí)間——lstat需要去進(jìn)行磁盤io才能得到這些信息,而目錄里的文件不像目錄項(xiàng)信息那樣緊密的存儲(chǔ)在一起,它們是分散的,所以一一讀取它們的元信息帶來的負(fù)擔(dān)會(huì)很大。 使用的系統(tǒng)調(diào)用太多了。由于我們測試目錄的文件很多,但getdents可能要調(diào)用多次,這里假設(shè)為兩次好了。對于每一個(gè)目錄項(xiàng),都需要用lstat去獲取文件的詳細(xì)信息,這樣又有5000次系統(tǒng)調(diào)用,加起來是5002次。系統(tǒng)調(diào)用的開銷是很大的,積累到5000多次則會(huì)帶來肉眼可見的性能下降。實(shí)際上linux本身對lstat有優(yōu)化,不會(huì)真的出現(xiàn)要反復(fù)進(jìn)入系統(tǒng)調(diào)用5000次的情況,但幾十到上百次還是需要的。
優(yōu)化的代碼其實(shí)只改了一行,是f.readdir(n, readdirDirEntry),第二個(gè)參數(shù)變了。新代碼會(huì)走上面注釋掉的那段邏輯:
// 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)
取代lstat的是函數(shù)newUnixDirent,這個(gè)函數(shù)可以不依賴額外的系統(tǒng)調(diào)用獲取文件的一部分元數(shù)據(jù):
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 }
文件名和類型都是在解析目錄項(xiàng)時(shí)就得到的,因此直接設(shè)置就行。不過不是每個(gè)文件系統(tǒng)都支持在目錄項(xiàng)數(shù)據(jù)里存儲(chǔ)文件類型,所以代碼里做了回退,一旦發(fā)現(xiàn)文件類型是無效數(shù)據(jù)就會(huì)使用lstat重新獲取信息。
如果只使用文件名和文件的類型這兩個(gè)信息,那么整個(gè)遍歷的邏輯流程到這就結(jié)束了,文件系統(tǒng)提供支持的情況下不需要調(diào)用lstat。所以整個(gè)遍歷只需要兩次系統(tǒng)調(diào)用。這就是為什么優(yōu)化方案會(huì)快接近五倍的原因。
對于要使用其他信息比如文件大小的用戶,優(yōu)化方案實(shí)際上也有好處,因?yàn)楝F(xiàn)在lstat是延遲且按需調(diào)用的:
func (d *unixDirent) Info() (FileInfo, error) { if d.info != nil { return d.info, nil } // 只會(huì)調(diào)用一次 return lstat(d.parent + "/" + d.name) }
這樣也能盡量減少不必要的系統(tǒng)調(diào)用。
所以整體優(yōu)化的原理是:盡量充分利用文件系統(tǒng)本身提供的信息+減少系統(tǒng)調(diào)用。要遍歷的目錄越大優(yōu)化的效果也越明顯。
優(yōu)化的支持情況
上面也說了,能做到優(yōu)化需要文件系統(tǒng)把文件類型信息存儲(chǔ)在目錄的目錄項(xiàng)數(shù)據(jù)里。這個(gè)需要文件系統(tǒng)的支持。
如果文件系統(tǒng)不支持的話最后還是需要依賴lstat去讀取具體文件的元數(shù)據(jù)。
不同文件系統(tǒng)的信息實(shí)在太分散,還有不少過時(shí)的,所以我花了幾天看代碼+查文檔做了下整理:
- btrfs,ext2,ext4:這個(gè)幾個(gè)文件系統(tǒng)支持優(yōu)化,man pages加文件系統(tǒng)代碼都能證實(shí)這一點(diǎn)
- OpenZFS:這個(gè)文件系統(tǒng)不在Linux內(nèi)核里,所以man pages里沒提到,但也支持優(yōu)化
- xfs:支持優(yōu)化,但得在創(chuàng)建文件系統(tǒng)時(shí)使用類似
mkfs.xfs -f -n ftype=1
的選項(xiàng)才行 - F2FS,EROFS:文檔沒提過,但看內(nèi)核的代碼里是支持的,代碼的位置在
xxx_readdir
這個(gè)函數(shù)附近。 - fat32,exfat:文檔沒提過,但看內(nèi)核代碼發(fā)現(xiàn)是支持的,不過fat家族的文件類型沒有那么多花樣,只有目錄和普通文件這兩種,所以代碼里很粗暴的判斷目錄項(xiàng)是否設(shè)置了dir標(biāo)志,有就是目錄沒有統(tǒng)統(tǒng)算普通文件。這么做倒是正常的,因?yàn)閒at本來就不支持別的文件類型,畢竟這個(gè)文件系統(tǒng)連軟鏈接都不支持,更不用指望Unix Domain Socket和命名管道了。
- ntfs:支持,然而如注釋所說,因?yàn)閚tfs和其他文件系統(tǒng)處理type的方式不一樣,導(dǎo)致雖然文件系統(tǒng)本身支持大部分文件類型,但type信息里只能獲得文件是不是目錄。所以它后面對于不是目錄的文件會(huì)去磁盤上讀取文件的inode然后再從inode里獲取文件類型——實(shí)際上相當(dāng)于執(zhí)行了一次lstat,相比lstat減少了進(jìn)入系統(tǒng)調(diào)用時(shí)的一次上下文切換,所以ntfs上優(yōu)化效果會(huì)不如其他文件系統(tǒng)。
這么一看的話基本上主流的常見的文件系統(tǒng)都支持這種優(yōu)化。
這也是為什么go1.16會(huì)引入這個(gè)優(yōu)化,不僅支持廣泛而且提升很大,免費(fèi)的加速誰不愛呢。
別的語言里怎么利用這個(gè)優(yōu)化
看到這里,你應(yīng)該發(fā)現(xiàn)這個(gè)優(yōu)化其實(shí)是系統(tǒng)層面的,golang只不過是適配了一下而已。
確實(shí)是這樣的,所以這個(gè)優(yōu)化不光golang能吃到,c/c++/python都行。
先說說c里怎么利用:直接用系統(tǒng)提供的readdir函數(shù)就行,這個(gè)函數(shù)會(huì)調(diào)用getdents,然后就能自然吃到優(yōu)化了。注意事項(xiàng)和go的一樣,需要檢測文件系統(tǒng)是否支持設(shè)置d_type。
c++:和c一樣,另外libstdc++的filesystem就是拿readdir實(shí)現(xiàn)的,所以用filesystem標(biāo)準(zhǔn)庫也能獲得優(yōu)化:
// 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 } // 如果操作系統(tǒng)以及文件系統(tǒng)不支持,則回退到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(); }
唯一的區(qū)別在于如果目標(biāo)文件是軟連接,也會(huì)調(diào)用stat。
python:使用os.scandir可以獲得優(yōu)化,底層和c一樣使用readdir:https://github.com/python/cpython/blob/main/Modules/posixmodule.c#L16211,實(shí)現(xiàn)方法甚至類名都和golang很像,代碼就不貼了。
總結(jié)
go雖然性能上一直被詬病,但在系統(tǒng)編程上倒是不含糊,基本常見的優(yōu)化都有做,可以經(jīng)常關(guān)注下新版本的release notes去看看go在這方面做的努力。
看著簡單的優(yōu)化,背后的可行性驗(yàn)證確實(shí)很復(fù)雜的,尤其是不同文件系統(tǒng)在怎么存儲(chǔ)額外的元數(shù)據(jù)上很不相同,光是看代碼就花了不少時(shí)間。
前面提到的ntfs在優(yōu)化效果上會(huì)打點(diǎn)折扣,所以我特意拿Windows設(shè)備測試了下,測試條件不變:
可以看到幾乎沒什么區(qū)別。如果不是看了linux的ntfs驅(qū)動(dòng),我是不知道會(huì)產(chǎn)生這樣的結(jié)果的。所以這個(gè)優(yōu)化Windows上效果不理想,但在Linux和MacOS上是適用的。
大膽假設(shè),小心求證,系統(tǒng)編程和性能優(yōu)化的樂趣也正在于此。
參考
exfat的fuse驅(qū)動(dòng)填充d_type的邏輯:https://github.com/relan/exfat/blob/master/libexfat/utils.c
Linux的ntfs驅(qū)動(dòng)需要獲取文件的inode才能得到正確的file type:https://github.com/torvalds/linux/blob/master/fs/ntfs3/dir.c
到此這篇關(guān)于Go1.16引入目錄遍歷優(yōu)化解析的文章就介紹到這了,更多相關(guān)目錄遍歷優(yōu)化解析內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Python實(shí)現(xiàn)快速保存微信公眾號(hào)文章中的圖片
這篇文章主要為大家詳細(xì)介紹了如何利用Python語言實(shí)現(xiàn)快速保存微信公眾號(hào)文章中的圖片,文中的示例代碼講解詳細(xì),感興趣的可以嘗試一下2022-06-06解決pycharm最左側(cè)Tool Buttons顯示不全的問題
今天小編就為大家分享一篇解決pycharm最左側(cè)Tool Buttons顯示不全的問題,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-12-12在Python中使用__slots__方法的詳細(xì)教程
這篇文章主要介紹了在Python中使用__slots__方法的詳細(xì)教程,__slots__方法是Python的一個(gè)重要內(nèi)置類方法,代碼基于Python2.x版本,需要的朋友可以參考下2015-04-04python3.6使用tkinter實(shí)現(xiàn)彈跳小球游戲
這篇文章主要為大家詳細(xì)介紹了python3.6使用tkinter實(shí)現(xiàn)彈跳小球游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-05-05Python光學(xué)仿真wxpython透鏡演示系統(tǒng)初始化與參數(shù)調(diào)節(jié)
這篇文章主要為大家介紹了Python光學(xué)仿真wxpython透鏡演示系統(tǒng)的初始化與參數(shù)調(diào)節(jié),同樣在學(xué)習(xí)wxpython透鏡演示系統(tǒng)的入門同學(xué)可以借鑒參考下,希望能夠有所幫助2021-10-10分布式全文檢索引擎ElasticSearch原理及使用實(shí)例
這篇文章主要介紹了分布式全文檢索引擎ElasticSearch原理及使用實(shí)例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-11-11python生成tensorflow輸入輸出的圖像格式的方法
本篇文章主要介紹了python生成tensorflow輸入輸出的圖像格式的方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-02-02Python辦公自動(dòng)化之發(fā)送電子郵件和Outlook集成
Python辦公?動(dòng)化是利?Python編程語?來創(chuàng)建腳本和程序,以簡化、加速和?動(dòng)化?常辦公任務(wù)和?作流程的過程,本文主要介紹一下如何利用Python實(shí)現(xiàn)發(fā)送電子郵件和Outlook集成,需要的可以參考下2023-12-12