Голанг: Как мне определить количество строк в файле?

В Golang я ищу эффективный способ определения количества строк, которые имеет файл.

Конечно, я всегда могу пропустить весь файл, но не очень эффективен.

file, _ := os.Open("/path/to/filename")
fileScanner := bufio.NewScanner(file)
lineCount := 0
for fileScanner.Scan() {
    lineCount++
}
fmt.Println("number of lines:", lineCount)

Есть ли лучший (более быстрый, дешевый) способ узнать, сколько строк имеет файл?

Ответы

Ответ 1

Здесь более быстрый счетчик строк, используя bytes.Count, чтобы найти символы новой строки.

Это быстрее, потому что он убирает всю дополнительную логику и буферизацию, необходимые для возврата целых строк, и использует некоторые оптимизированные по сборке функции, предлагаемые пакетом байтов, для поиска символов в байтовом фрагменте.

Более крупные буферы также помогают здесь, особенно с большими файлами. В моей системе, с файлом, который я использовал для тестирования, 32k-буфер был самым быстрым.

func lineCounter(r io.Reader) (int, error) {
    buf := make([]byte, 32*1024)
    count := 0
    lineSep := []byte{'\n'}

    for {
        c, err := r.Read(buf)
        count += bytes.Count(buf[:c], lineSep)

        switch {
        case err == io.EOF:
            return count, nil

        case err != nil:
            return count, err
        }
    }
}

и результат теста:

BenchmarkBuffioScan   500      6408963 ns/op     4208 B/op    2 allocs/op
BenchmarkBytesCount   500      4323397 ns/op     8200 B/op    1 allocs/op
BenchmarkBytes32k     500      3650818 ns/op     65545 B/op   1 allocs/op

Ответ 2

Самый эффективный способ, который я нашел, - это использование IndexByte байтового пакета, он как минимум в четыре раза быстрее, чем при использовании bytes.Count и в зависимости от размера буфера он использует гораздо меньше памяти.

func lineCounter(r io.Reader) (int, error) {

    var readSize int
    var err error
    var count int

    buf := make([]byte, 1024)

    for {
        readSize, err = r.Read(buf)
        if err != nil {
            break
        }

        var buffPosition int
        for {
            i := bytes.IndexByte(buf[buffPosition:], '\n')
            if i == -1 || readSize == buffPosition {
                break
            }
            buffPosition += i + 1
            count++
        }
    }
    if readSize > 0 && count == 0 || count > 0 {
        count++
    }
    if err == io.EOF {
        return count, nil
    }

    return count, err
}

эталонный тест

BenchmarkIndexByteWithBuffer  2000000          653 ns/op        1024 B/op          1 allocs/op
BenchmarkBytes32k             500000          3189 ns/op       32768 B/op          1 allocs/op

Ответ 3

Нет никакого подхода, который значительно быстрее вашего, поскольку нет метаданных о том, сколько строк имеет файл. Вы можете немного ускорить поиск вручную символов новой строки:

func lineCount(r io.Reader) (int n, error err) {
    buf := make([]byte, 8192)

    for {
        c, err := r.Read(buf)
        if err != nil {
            if err == io.EOF && c == 0 {
                break
            } else {
                return
            }
        }

        for _, b := range buf[:c] {
            if b == '\n' {
                n++
            }
        }
    }

    if err == io.EOF {
        err = nil
    }
}