使用 io.Writer 时避免在 golang 中分配过多的内存

Posted

技术标签:

【中文标题】使用 io.Writer 时避免在 golang 中分配过多的内存【英文标题】:Avoiding excessive memory allocation in golang when using an io.Writer 【发布时间】:2016-01-20 17:30:17 【问题描述】:

我正在开发一个名为 redis-mass 的 Go 命令行工具,它将一堆 redis 命令转换为 redis protocol format。

第一步是将 node.js 版本移植到 Go。我使用ioutil.ReadFile(inputFileName) 获取文件的字符串版本,然后返回一个编码字符串作为输出。

当我在一个包含 2,000,000 个 redis 命令的文件上运行它时,大约需要 8 秒,而节点版本大约需要 16 秒。我猜它只有两倍快的原因是因为它首先将整个文件读入内存,所以我将我的编码函数更改为接受一对(raw io.Reader, enc io.Writer),它看起来像这样:

func EncodeStream(raw io.Reader, enc io.Writer) 
    var args []string
    var length int

    scanner := bufio.NewScanner(raw)

    for scanner.Scan() 
            command := strings.TrimSpace(scanner.Text())
            args = parse(command)
            length = len(args)
            if length > 0 
                    io.WriteString(enc, fmt.Sprintf("*%d\r\n", length))
                    for _, arg := range args 
                            io.WriteString(enc, fmt.Sprintf("$%d\r\n%s\r\n", len(arg), arg))
                    
            
    

不过,这在 200 万行的文件上花了 12 秒,所以我使用 github.com/pkg/profile 来查看它是如何使用内存的,看起来内存分配的数量很大:

# Alloc = 3162912
# TotalAlloc = 1248612816
# Mallocs = 46001048
# HeapAlloc = 3162912

我可以限制io.Writer 使用固定大小的缓冲区并避免所有这些分配吗?

更一般地说,在这种方法中如何避免过度分配? Here's the full source for more context

【问题讨论】:

io.Writer 没有缓冲区,它是一个接口。在你的代码中,你为什么要缓冲一个缓冲区? 谁说你所有的allocs都在io.Writer中?你认为command := strings.TrimSpace(scanner.Text()) 做了什么?我认为它负责大约 200 万次分配。 fmt.Sprintf 也会导致大量分配。 对,我问的不是正确的问题,另外,很好,我已经在最新的 master 中解除了我的 bytes.Buffer 的缓冲 如果您想减少分配,避免字符串操作很有用。尽可能多地使用和重用 []byte 切片。 【参考方案1】:

通过使用 []byte 而不是字符串来减少分配。 fmt.Printf 直接输出而不是 fmt.Sprintf 和 io.WriteString。

func EncodeStream(raw io.Reader, enc io.Writer) 
    var args []string
    var length int

    scanner := bufio.NewScanner(raw)

    for scanner.Scan() 
            command := bytes.TrimSpace(scanner.Bytes())
            args = parse(command)
            length = len(args)
            if length > 0 
                    fmt.Printf(enc, "*%d\r\n", length))
                    for _, arg := range args 
                           fmt.Printf(enc, "$%d\r\n%s\r\n", len(arg), arg))
                    
            
    

【讨论】:

以上是关于使用 io.Writer 时避免在 golang 中分配过多的内存的主要内容,如果未能解决你的问题,请参考以下文章

golang io.Reader和io.Writer很有趣

Go 入门很简单:Writer和Reader接口

golang 中通过strings/bytes/bufio 等包实现相关IO

golang中bufio包

golang 缓冲区的终端输入

Golang中读取文件最常见的错误