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

Performance 如何提高Go中逐行读取大文件的速度,performance,go,Performance,Go,我试图找出逐行读取大型文件并检查该行是否包含字符串的最快方法。我正在测试的文件大约有680mb大 主程序包 进口( “布菲奥” “fmt” “操作系统” “字符串” ) func main(){ f、 错误:=os.Open(“./crackstation-human-only.txt”) 扫描仪:=bufio.NewScanner(f) 如果错误!=零{ 恐慌(错误) } 延迟f.关闭() 对于scanner.Scan(){ if strings.Contains(scanner.Text()

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

主程序包
进口(
“布菲奥”
“fmt”
“操作系统”
“字符串”
)
func main(){
f、 错误:=os.Open(“./crackstation-human-only.txt”)
扫描仪:=bufio.NewScanner(f)
如果错误!=零{
恐慌(错误)
}
延迟f.关闭()
对于scanner.Scan(){
if strings.Contains(scanner.Text(),“Iforgotmypassword”){
fmt.Println(scanner.Text())
}
}
}
在我的机器上构建程序并计时后,它运行了3秒钟
/speed 3.13s用户1.25s系统122%cpu总量3.563

增加缓冲区后

buf := make([]byte, 64*1024)
scanner.Buffer(buf, bufio.MaxScanTokenSize)
情况好转了一点
/speed 2.47s用户0.25s系统104%cpu 2.609总计

我知道它可以变得更好,因为其他工具可以在一秒钟内完成,而无需任何索引。这种方法的瓶颈是什么


0.33s用户0.14s系统94%cpu 0.501总计

您可以尝试使用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()
lines:=make(chan string,numWorkers*2)//给频道足够的空间放很多东西,这样阅读器就不会被阻塞

go func(scanner*bufio.scanner,out上次编辑

这是一个“逐行”的解决方案,它可以打印出整个匹配行,从而解决需要花费大量时间的问题

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]))
    }
}

原始答案

如果您只需要查看字符串是否在文件中,为什么不使用regex呢

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

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的混乱文本,包含换行符

时间运行。/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秒。

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

格雷普的成绩是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")
    }
}

H.Ross的答案非常棒,但它会将整个文件读入内存,如果文件太大,这可能不可行。如果您仍然希望逐行扫描,或者如果您正在搜索多个项目,我发现使用scanner.Bytes()而不是scanner.Text()在我的机器上稍微提高了速度,从原始问题的2.244s提高到1.608s。bufio的scanner.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())
        }
    }
}

scanner.Text
需要字符串转换,因此改用
scanner.Bytes
Bytes.Contains
而不是
string.Contains
可能会更快。
fmt.Println
也不是最快的,正如所写的那样,需要第二次进行字符串转换。但一般来说,您应该分析t他编写代码以查找速度下降的地方。删除字符串转换也没有帮助。如果我只有一个行计数器
计数器:=0 for scanner.Scan(){counter++}
它不会变快,只有一点点。因此,看起来引导项是scanner.Scan()但不知道如何改进。您是否尝试过设置更大的缓冲区?这里的代码中也没有计时信息,您是如何运行您的程序的?只是想确认一下,您是在计时一个构建的可执行文件,还是在计时
运行
?time./programname,它提供了
/programname 2.53s用户0.49s系统99%cpu 3.025总计
扫描程序仍在运行,似乎速度很慢。我尝试过这个,它比原始版本慢得多,有8个工作人员,85秒,而原始版本为2.2秒。调度开销和复制到缓冲区的时间比处理过程要长。还有一个bug!应该是
fmt.Println(text)
,而不是
fmt.Println(scanner.Text())
,否则它会将不同的行打印到匹配的行。这确实会提高速度
/speed 0.35s user 0.49s system 99%cpu 0.845 total
,但我试图理解的是,当逐行打印时,瓶颈在哪里感谢详尽的答案。按照这个逻辑,将缓冲区增加到1GB将使它更安全同样快,因为所有内容都在内存中,但情况似乎并非如此。它仍然运行得很慢。这确实很快而且有效,但我仍然不明白为什么逐行解决方案失败。逐行解决方案需要检查每个字节一次,看它是否是换行符,根据换行符的位置将这些字节分成块,然后根据你要查找的字符串再次检查这些块。因为你
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())
        }
    }
}