高效追加可变长度的字符串容器(Golang)

时间:2013-11-27 19:56:58

标签: go containers slice

问题:

我需要将多个正则表达式应用于大日志文件的每一行(例如几GB长),收集非空匹配并将它们全部放在一个数组中(用于序列化并通过网络发送)。

如果对this question的回答成立,那么切片帮助不大:

  

如果切片没有足够的容量,追加将需要分配新内存并复制旧内存。对于具有< 1024个元素的切片,它将使容量加倍,对于具有> 1024个元素的切片,它将使其增加1.25倍。

由于可能存在数十万个正则表达式匹配,我无法真正预测切片的长度/容量。我不能把它变得太大“以防万一”bc这会浪费内存(或者它会吗?如果内存分配器足够智能不分配太多未写入的内存,也许我可以使用一个巨大的切片容量没有太大的伤害?)。

所以我正在考虑采用以下方法:

  1. 有一个双向链接的匹配列表(http://golang.org/pkg/container/list/
  2. 计算其长度(len()会工作吗?)
  3. 预分配此容量的一部分
  4. 复制此切片的字符串指针
  5. 在Go中实现这个目标是否有一种不太费力的方法(附加~O(1)追加复杂性)?

    (当然是golang新手)

2 个答案:

答案 0 :(得分:14)

append()平均(摊销)成本已经是O(1),因为它每次都会增加一个百分比的数组。随着阵列变大,增长越来越昂贵,但比例也越来越少。一个10M项目的片段比1M片段片段的成本高10倍,但由于我们分配的额外容量与大小成正比,因此它也将是10 append(slice, item)次呼叫的直到下一次它成长。增加的成本和降低的重新分配频率抵消了,使平均成本保持不变,即O(1)。

同样的想法也适用于其他语言的动态大小的数组:例如,Microsoft的std::vector实现显然每次增加50%的数组。摊销O(1)并不意味着您不需要为分配支付任何费用,只是您继续以与阵列变大相同的平均费率支付。

在我的笔记本电脑上,我可以在77毫秒内运行一百万slice = append(slice, someStaticString)秒。下面提到的快速的一个原因是,“复制”字符串以扩大数组实际上只是复制字符串标题(指针/长度对),而不是复制内容。与您正在使用的其他数据量相比,100,000个字符串标题仍然不足2MB进行复制。

在微基准测试中,

container/list对我来说慢了3倍;当然,链接列表追加也是恒定时间,但我认为append具有较低的常量,因为它通常只能写入几个字的内存而不分配列表项等。时间码赢了'在Playground中工作,但您可以在本地复制并运行它以查看自己:http://play.golang.org/p/uYyMScmOjX


但是你在这里问一个更具体的问题,关于一个类似grep的应用程序(并且感谢你用上下文询问一个详细的问题)。为此,底线建议是,如果你正在搜索日志,那么最好避免在RAM中缓冲整个输出。

您可以编写一些内容以将结果作为单个函数进行流式处理:logparser.Grep(in io.Reader, out io.Writer, patterns []regexp.Regexp);如果您不希望发送结果的代码与grep代码过于混淆,您可以选择out chan []bytefunc(match []byte) (err error)

(在[]bytestring之间:[]byte似乎可以在此处完成工作,并避免在您进行[]byte< => string次转化时做I / O,所以我更喜欢。我不知道你在做什么,如果你需要string,那很好。)

如果你将整个匹配列表保存在RAM中,请注意保持对大字符串或字节切片的一部分的引用会使整个源字符串/切片不被垃圾回收。因此,如果你走这条路线,那么违反直觉,你可能想要复制匹配,以避免将所有源日志数据保存在RAM中。

答案 1 :(得分:3)

我试图将你的问题提炼成一个非常简单的例子。

由于可能存在“数十万个正则表达式匹配”,因此我为matches切片容量进行了大量的1 M(1024 * 1024)条目初始分配。切片是参考类型。切片头“struct”在64位操作系统上具有长度,容量和指针,总共24(3 * 8)个字节。因此,对于1M个条目的片段的初始分配仅为24(24 * 1)MB。如果有超过1个M条目,则将分配容量为1.25(1 + 1/4)M条目的新切片,并将现有的1 M切片报头条目(24 MB)复制到其中。

总之,通过最初分配切片容量,可以避免许多append的大量开销。更大的内存问题是为每个匹配保存和引用的所有数据。更大的CPU时间问题是执行regexp.FindAll的时间。

package main

import (
    "bufio"
    "fmt"
    "os"
    "regexp"
)

var searches = []*regexp.Regexp{
    regexp.MustCompile("configure"),
    regexp.MustCompile("unknown"),
    regexp.MustCompile("PATH"),
}

var matches = make([][]byte, 0, 1024*1024)

func main() {
    logName := "config.log"
    log, err := os.Open(logName)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    defer log.Close()
    scanner := bufio.NewScanner(log)
    for scanner.Scan() {
        line := scanner.Bytes()
        for _, s := range searches {
            for _, m := range s.FindAll(line, -1) {
                matches = append(matches, append([]byte(nil), m...))
            }
        }
    }
    if err := scanner.Err(); err != nil {
        fmt.Fprintln(os.Stderr, err)
    }
    // Output matches
    fmt.Println(len(matches))
    for i, m := range matches {
        fmt.Println(string(m))
        if i >= 16 {
            break
        }
    }
}