如何提高在Go中逐行读取大文件的速度

时间:2019-03-05 20:06:31

标签: performance go

我试图找出最快的方法来逐行读取大文件,并检查该行是否包含字符串。我正在测试的文件大约为680mb

    package main

    import (
        "bufio"
        "fmt"
        "os"
        "strings"
    )

    func main() {
        f, err := os.Open("./crackstation-human-only.txt")

        scanner := bufio.NewScanner(f)
        if err != nil {
            panic(err)
        }
        defer f.Close()

        for scanner.Scan() {
            if strings.Contains(scanner.Text(), "Iforgotmypassword") {
                fmt.Println(scanner.Text())
            }
        }
    }

构建程序并在我的计算机上对其计时后,它将运行3秒钟以上 ./speed 3.13s user 1.25s system 122% cpu 3.563 total

增加缓冲区后

buf := make([]byte, 64*1024)
scanner.Buffer(buf, bufio.MaxScanTokenSize)

好一点了 ./speed 2.47s user 0.25s system 104% cpu 2.609 total

我知道它会变得更好,因为其他工具可以在不到任何索引的情况下在一秒钟内完成它。这种方法的瓶颈似乎是什么?

0.33s user 0.14s system 94% cpu 0.501 total

4 个答案:

答案 0 :(得分:2)

最后编辑

这是一个耗时的问题的“逐行”解决方案,它打印了整个匹配行。

package main

import (
    "bytes"
    "fmt"
    "io/ioutil"
)

func main() {
    dat, _ := ioutil.ReadFile("./jumble.txt")
    i := bytes.Index(dat, []byte("Iforgotmypassword"))
    if i != -1 {
        var x int
        var y int
        for x = i; x > 0; x-- {
            if dat[x] == byte('\n') {
                break
            }
        }
        for y = i; y < len(dat); y++ {
            if dat[y] == byte('\n') {
                break
            }
        }
        fmt.Println(string(dat[x : y+1]))
    }
}
real    0m0.421s
user    0m0.068s
sys     0m0.352s

原始答案

如果只需要查看字符串是否在文件中,为什么不使用正则表达式?

注意:我将数据保留为字节数组,而不是转换为字符串。

package main

import (
    "fmt"
    "io/ioutil"
    "regexp"
)

var regex = regexp.MustCompile(`Ilostmypassword`)

func main() {
    dat, _ := ioutil.ReadFile("./jumble.txt")
    if regex.Match(dat) {
        fmt.Println("Yes")
    }
}

jumble.txt是859 MB的混乱文本,其中包括换行符。

使用time ./code运行,我得到:

real    0m0.405s
user    0m0.064s
sys     0m0.340s

<罢工> 要尝试回答您的评论,我认为瓶颈并不是固有地逐行搜索,Golang使用一种高效的算法来搜索字符串/符文。

我认为瓶颈来自IO读取,当程序从文件读取时,它通常不在读取队列中排在第一位,因此,程序必须等到可以读取后才能开始实际比较。因此,当您一遍又一遍地阅读时,您被迫等待输入IO。

要给您一些数学知识,如果您的缓冲区大小为64 * 1024(或65535字节),并且文件为1 GB。划分1 GB / 65535字节需要检查整个文件所需的15249次读取。就像我的方法一样,我“一次”读取整个文件,并对照该构造的数组进行检查。

我能想到的另一件事就是遍历文件所需的全部循环以及每个循环所需的时间:

给出以下代码:

dat, _ := ioutil.ReadFile("./jumble.txt")
sdat := bytes.Split(dat, []byte{'\n'})
for _, l := range sdat {
    if bytes.Equal([]byte("Iforgotmypassword"), l) {
        fmt.Println("Yes")
    }
}

我计算出每个循环平均需要32纳秒,字符串Iforgotmypassword在文件的100000000行上,因此此循环的执行时间约为32纳秒* 100000000〜= 3.2秒。

答案 1 :(得分:1)

使用我自己的700MB测试文件和原始文件,时间仅超过7秒

使用grep时为0.49秒

使用此程序(不会打印出该行,它只是说是) 0.082秒

package main

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

func check(e error) {
    if e != nil {
        panic(e)
    }
}
func main() {
    find := []byte(os.Args[1])
    dat, err := ioutil.ReadFile("crackstation-human-only.txt")
    check(err)
    if bytes.Contains(dat, find) {
        fmt.Print("yes")
    }
}

答案 2 :(得分:1)

H。 Ross的回答很棒,但是它将整个文件读入内存,如果文件太大,这可能不可行。如果您仍然想逐行扫描,或者如果您正在搜索多个项目,我发现使用scanner.Bytes()而不是scanner.Text()可以使我的计算机上的速度从2.244s略微提高。最初的问题是1.608s。 bufio的scan.Bytes()方法不会分配任何额外的内存,而Text()会从其缓冲区创建一个字符串。

package main

import (
    "bufio"
    "fmt"
    "os"
    "bytes"
)

// uses scanner.Bytes to avoid allocation.
func main() {
    f, err := os.Open("./crackstation-human-only.txt")

    scanner := bufio.NewScanner(f)
    if err != nil {
        panic(err)
    }
    defer f.Close()

    toFind := []byte("Iforgotmypassword")

    for scanner.Scan() {
        if bytes.Contains(scanner.Bytes(), toFind) {
            fmt.Println(scanner.Text())
        }
    }
}

答案 3 :(得分:-3)

您可以尝试使用goroutines并行处理多行:

lines := make(chan string, numWorkers * 2) // give the channel enough room to put lots of things in so the reader isn't blocked

go func(scanner *bufio.Scanner, out <-chan string) {
  for scanner.Scan() {
    out <- scanner.Text()
  }
  close(out)
}(scanner, lines)

var wg sync.WaitGroup
wg.Add(numWorkers)

for i := 0; i < numWorkers; i++ {
  go func(in chan<- string) {
    defer wg.Done()
    for text := range in {
      if strings.Contains(text, "Iforgotmypassword") {
        fmt.Println(scanner.Text())
      }
    }
  }(lines)
}

wg.Wait()

我不确定这能真正加快多少速度,这取决于您可用的硬件类型;听起来您正在寻求5倍以上的速度提升,因此您可能会注意到,如果您正在运行的东西可以支持8个并行工作线程。随时使用很多 worker-goroutines。祝你好运。