传递生成的数据作为io.Reader在GO中高速阅读器
#go #benchmark

当您想基准一个处理io.Reader的代码时,通常使用io.Pipe与额外的io.Writer进行流式传输数据。

看这个微不足道的示例基准。

func Benchmark_ioPipe(b *testing.B) {
    r, w := io.Pipe()
    i := 0

    go func() {
        for {
            i++

            if i == b.N {
                _ = w.Close()

                return
            }

            _, _ = w.Write([]byte("<this might be a dynamic piece of generated data>\n"))
        }
    }()

    b.ReportAllocs()

    // io.Copy acts as a dummy processor here, in real-world 
    // scenario you'd have an actual io.Reader consumer.
    if _, err := io.Copy(io.Discard, r); err != nil {
        b.Fatal(err.Error())
    }
}
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
Benchmark_ioPipe-12      1655488           729.8 ns/op        64 B/op          1 allocs/op

这种饲料的性能还不错,但也不是令人印象深刻的。牺牲了一些速度以在并发下实现数据安全,读取和写入都可以与管道中发生的同步并行调用。

让我们尝试提高针对io.Reader的特定饲料的特定情况,不需要外部同步。

type pagesReader struct {
    // next returns contents of the next page, 
    // io.EOF error indicates last page.
    next func() ([]byte, error)

    // buf keeps the data to be read.
    buf []byte
}

func (r *pagesReader) Read(p []byte) (n int, err error) {
    // Fill the reader buffer with pages 
    // until it exceeds the incoming buffer 
    // or reaches the end of pages.
    for len(r.buf) < len(p) {
        page, err := r.next()
        r.buf = append(r.buf, page...)
        if err != nil {
            copy(p, r.buf)
            return len(r.buf), err
        }
    }

    // Put head of reader buffer in the incoming buffer.
    copy(p, r.buf)

    // Move remaining tail into the head of reader buffer.
    remaining := r.buf[len(p):]
    r.buf = r.buf[:len(remaining)]
    copy(r.buf, remaining)

    return len(p), nil
}

pagesReader实现了io.Reader,从用户定义的回调next func() ([]byte, error)

现在,让我们更新以前使用pagesReader而不是io.Pipe的基准。

func Benchmark_pagesReader(b *testing.B) {
    i := 0
    r := &pagesReader{
        next: func() ([]byte, error) {
            i++

            if i == b.N {
                return nil, io.EOF
            }

            return []byte("<this might be a dynamic piece of generated data>\n"), nil
        },
    }

    b.ReportAllocs()

    // io.Copy acts as a dummy processor here, in real-world
    // scenario you'd have an actual io.Reader consumer.
    if _, err := io.Copy(io.Discard, r); err != nil {
        b.Fatal(err.Error())
    }
}
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
Benchmark_pagesReader-12        21766588            52.38 ns/op       64 B/op          1 allocs/op

非同步实现的速度要快得多(大约14倍),可能更适合基准,以降低对整体结果的影响。

当需要同步时,可以在next函数本身中使用MUTEX对其进行管理。