首页 > 解决方案 > 使用 bufio.NewScanner 提高阅读性能

问题描述

一个服务于一个目的的简单程序:

  1. 逐行读取脚本文件,创建一个字符串,同时忽略任何空白的新行或注释(包括 shebang)。添加一个';' 如果需要,在一行的末尾。(我知道,我知道,反斜杠和 & 号等)

我的问题是:

如何提高这个小程序的性能?在另一个答案中,我读过关于使用scanner.Bytes()而不是scanner.Text(),但这似乎不可行,因为我想要的是字符串。

带有测试文件的示例代码:https: //play.golang.org/p/gzSTLkP3BoB

这是一个简单的程序:

func main() {
    file, err := os.Open("./script.sh")
    if err != nil {
        log.Fatalln(err)
    }
    defer file.Close()

    var a strings.Builder
    scanner := bufio.NewScanner(file)

    for scanner.Scan() {
        lines := scanner.Text()

        switch {
        case lines == "" || lines[:1] == "#":
            continue
        case lines[len(lines)-1:] != ";":
            a.WriteString(lines + "; ")
        default:
            a.WriteString(lines + " ")
        }
    }

    fmt.Println(a.String())
}

标签: go

解决方案


我用strings.Builderandioutil.ReadAll来提高性能。当您处理小型 shell 脚本时,我认为一次读取文件不应该对内存造成压力(我使用过ioutil.ReadAll)。我还只分配了一次,以便为strings.Builder减少分配提供足够的存储空间。

  • doFast:更快的实现
  • doSlow:较慢的实现(您最初所做的)

现在,让我们看一下基准测试结果:

goos: darwin
goarch: amd64
pkg: test
cpu: Intel(R) Core(TM) i5-1038NG7 CPU @ 2.00GHz
BenchmarkDoFast-8         342602          3334 ns/op        1280 B/op          3 allocs/op
BenchmarkDoSlow-8         258896          4408 ns/op        4624 B/op          8 allocs/op
PASS
ok      test    2.477s

我们可以看到,这doFast不仅更快,而且分配更少。衡量的指标越低越好。

package main

import (
    "bufio"
    "bytes"
    "fmt"
    "io/ioutil"
    "os"
    "strings"
)

func open(filename string) (*os.File, error) {
    return os.Open(filename)
}

func main() {
    fd, err := open("test.sh")
    if err != nil {
        panic(err)
    }
    defer fd.Close()

    outputA, err := doFast(fd)
    if err != nil {
        panic(err)
    }

    fd.Seek(0, 0)
    outputB, err := doSlow(fd)
    if err != nil {
        panic(err)
    }

    fmt.Println(outputA)
    fmt.Println(outputB)
}

func doFast(fd *os.File) (string, error) {
    b, err := ioutil.ReadAll(fd)
    if err != nil {
        return "", err
    }

    var res strings.Builder
    res.Grow(len(b))

    bLines := bytes.Split(b, []byte("\n"))

    for i := range bLines {
        switch {
        case len(bLines[i]) == 0 || bLines[i][0] == '#':
        case bLines[i][len(bLines[i])-1] != ';':
            res.Write(bLines[i])
            res.WriteString("; ")
        default:
            res.Write(bLines[i])
            res.WriteByte(' ')
        }
    }

    return res.String(), nil
}

func doSlow(fd *os.File) (string, error) {
    var a strings.Builder
    scanner := bufio.NewScanner(fd)

    for scanner.Scan() {
        lines := scanner.Text()

        switch {
        case lines == "" || lines[:1] == "#":
            continue
        case lines[len(lines)-1:] != ";":
            a.WriteString(lines + "; ")
        default:
            a.WriteString(lines + " ")
        }
    }

    return a.String(), nil
}

注意:我没有使用bufio.NewScanner; 是必需的吗?


推荐阅读