golang bufio、ioutil讀文件的速度比較(性能測試)和影響因素分析

前言

golang讀取文件的方式主要有4種:golang

  1. 使用File自帶的Read方法
  2. 使用bufio庫的Read方法
  3. 使用io/ioutil庫的ReadAll()
  4. 使用io/ioutil庫的ReadFile()

關於前3種方式的速度比較,我最先是在 GoLang幾種讀文件方式的比較 看過,但在該blog的評論區有人(study_c)提出了質疑,並提供了測試代碼。根據該代碼的測試,結果應該是數組

bufio > ioutil.ReadAll > File自帶Read緩存

在我反覆跑study_c測試代碼過程當中發現幾個問題或者說是影響因素:dom

  • Read()每次讀取的塊的大小對結果也是有影響的
  • 連續測試同一個文件,會從系統緩存或是SSD緩存加載文件,後面的測試結果會被加速

因此本文的性能測試就是基於study_c的代碼的基礎上作了修改,嘗試測試不一樣塊大小對結果的影響,並增長對ioutil.ReadFile()的測試,還有隨機生成文件以應對緩存影響公平性。函數

性能測試

測試環境性能

CPU: i5-6300HQ
MEM: 12GB
DSK: SANDISK Extreme PRO SSD 480GB
OS : WIN 10 64bit測試

測試代碼1【randfiles.go】,生成1-500MB包含隨機字符串的文件spa

package main

import (
    "math/rand"
    "fmt"
    "flag"
    "strconv"
    "io/ioutil"
)

const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
// https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-golang
func RandStringBytes(n int) []byte {
    b := make([]byte, n)
    for i := range b {
        b[i] = letterBytes[rand.Intn(len(letterBytes))]
    }
    return b
}
func RandFile(path string,filesizeMB int) {
    b:=RandStringBytes(filesizeMB * 1024)    //生成1-500KB大小的隨機字符串
    bb := make([]byte, filesizeMB * 1024 * 1024)
    for i:=0;i<1024;i++ {    //複製1024遍
        copy(bb[len(b)*i:len(b)*(i+1)],b)
    }
    //fmt.Printf("%s",b)
    ioutil.WriteFile(path,bb,0666)
}
func main() {
    flag.Parse()
    filesizeMB,err :=strconv.Atoi(flag.Arg(0))        //1-500MB大小的文件
    if err != nil{panic(err)}
    if filesizeMB > 500 {panic("too large file,>500MB")}
    RandFile("./random1.txt",filesizeMB)
    RandFile("./random2.txt",filesizeMB)
    RandFile("./random3.txt",filesizeMB)
    RandFile("./random4.txt",filesizeMB)
    fmt.Printf("Created 4 files, each file size is %d MB.",filesizeMB)
}

測試代碼2【speed.go】,性能測試code

package main

import(
    "fmt"
    "os"
    "flag"
    "io"
    "io/ioutil"
    "bufio"
    "time"
    "strconv"
)

func read1(path string,blocksize int){
    fi,err := os.Open(path)
    if err != nil{
        panic(err)
    }
    defer fi.Close()
    block := make([]byte,blocksize)
    for{
        n,err := fi.Read(block)
        if err != nil && err != io.EOF{panic(err)}
        if 0 ==n {break}
    }
}

func read2(path string,blocksize int){
    fi,err := os.Open(path)
    if err != nil{panic(err)}
    defer fi.Close()
    r := bufio.NewReader(fi)
    block := make([]byte,blocksize)
    for{
        n,err := r.Read(block)
        if err != nil && err != io.EOF{panic(err)}
        if 0 ==n {break}
    }
}

func read3(path string){
    fi,err := os.Open(path)
    if err != nil{panic(err)}
    defer fi.Close()
    _,err = ioutil.ReadAll(fi)
}

func read4(path string){
    _,err := ioutil.ReadFile(path)
    if err != nil{panic(err)}
}

func main(){
    flag.Parse()
    file1 := "./random1.txt"
    file2 := "./random2.txt"
    file3 := "./random3.txt"
    file4 := "./random4.txt"
    blocksize,_ :=strconv.Atoi(flag.Arg(0))
    var start,end time.Time
    start = time.Now()
    read1(file1,blocksize)
    end = time.Now()
    fmt.Printf("file/Read() cost time %v\n",end.Sub(start))
    start = time.Now()
    read2(file2,blocksize)
    end = time.Now()
    fmt.Printf("bufio/Read() cost time %v\n",end.Sub(start))
    start = time.Now()
    read3(file3)
    end = time.Now()
    fmt.Printf("ioutil.ReadAll() cost time %v\n",end.Sub(start))
    start = time.Now()
    read4(file4)
    end = time.Now()
    fmt.Printf("ioutil.ReadFile() cost time %v\n",end.Sub(start))
}

測試結果:
圖片描述
測試1:塊大小爲4KB,這是個常見的大小,出人意料ioutil.ReadAll()最慢
測試2:塊大小爲1KB,這是前言提到的測試結果所用的塊大小,與其測試結果一致
測試3:塊大小爲32KB,在大塊的狀況下,調用Read()次數更少,bufio已經沒有優點,但前二者卻遠快於ioutil包的兩個函數
測試4:塊大小爲16字節,在小塊的狀況下,沒有緩存的文件普通Read成績慘不忍睹blog

影響因素

在查閱golang標準庫的源代碼後,之因此有不一樣的結果是與每一個方法的實現相關的,最大的因素就是內部buffer的大小,這個直接決定了讀取的快慢:

  1. f.Read()底層實現是系統調用syscall.Read(),沒有深究
  2. bufio.NewReader(f)實際調用NewReaderSize(f, defaultBufSize),而defaultBufSize=4096,能夠直接用bufio.NewReaderSize(f,32768)來預分配更大的緩存,緩存的實質是make([]byte, size)
  3. ioutil.ReadAll(f)實際調用readAll(r, bytes.MinRead),而bytes.MinRead=512,緩存的實質是bytes.NewBuffer(make([]byte, 0, 512),雖然bytes.Buffer會根據狀況自動增大,但每次從新分配都會影響性能
  4. ioutil.ReadFile(path)是調用readAll(f, n+bytes.MinRead),這個n取決於文件大小,文件小於10^9字節(0.93GB),n=文件大小,就是NewBuffer一個略大於文件大小的緩存,很是慷慨;大於則n=0,好慘,也就是說大於1G的文件就跟ioutil.ReadAll(f)一個樣子了。
  5. 但全量緩存的ReadFile爲何不如大塊讀取的前二者呢?我猜想是NewBuffer包裝的字節數組性能固然不如裸奔的字符數組。。

結論

  • 當每次讀取塊的大小小於4KB,建議使用bufio.NewReader(f), 大於4KB用bufio.NewReaderSize(f,緩存大小)
  • 要讀Reader, 圖方便用ioutil.ReadAll()
  • 一次性讀取文件,使用ioutil.ReadFile()
  • 不一樣業務場景,選用不一樣的讀取方式
相關文章
相關標籤/搜索