zoukankan      html  css  js  c++  java
  • golang bufio、ioutil读文件的速度比较(性能测试)和影响因素分析

    前言

    golang读取文件的方式主要有4种:

    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测试代码过程中发现几个问题或者说是影响因素:

    • 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包含随机字符串的文件

    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】,性能测试

    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
    ",end.Sub(start))
        start = time.Now()
        read2(file2,blocksize)
        end = time.Now()
        fmt.Printf("bufio/Read() cost time %v
    ",end.Sub(start))
        start = time.Now()
        read3(file3)
        end = time.Now()
        fmt.Printf("ioutil.ReadAll() cost time %v
    ",end.Sub(start))
        start = time.Now()
        read4(file4)
        end = time.Now()
        fmt.Printf("ioutil.ReadFile() cost time %v
    ",end.Sub(start))
    }

    测试结果:

    测试1:块大小为4KB,这是个常见的大小,出人意料ioutil.ReadAll()最慢
    测试2:块大小为1KB,这是前言提到的测试结果所用的块大小,与其测试结果一致
    测试3:块大小为32KB,在大块的情况下,调用Read()次数更少,bufio已经没有优势,但前两者却远快于ioutil包的两个函数
    测试4:块大小为16字节,在小块的情况下,没有缓存的文件普通Read成绩惨不忍睹

    影响因素

    在查阅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()
    • 不同业务场景,选用不同的读取方式
  • 相关阅读:
    Delphi XE5 android 蓝牙通讯传输
    Delphi XE5 android toast
    Delphi XE5 android openurl(转)
    Delphi XE5 如何设计并使用FireMonkeyStyle(转)
    Delphi XE5 android 捕获几个事件
    Delphi XE5 android listview
    Delphi XE5 android 黑屏的临时解决办法
    Delphi XE5 android popumenu
    Delphi XE5 android 获取网络状态
    Delphi XE5 android 获取电池电量
  • 原文地址:https://www.cnblogs.com/276815076/p/8615500.html
Copyright © 2011-2022 走看看