有效附加到字符串可变长度容器(Golang)
问题是: 我需要对一个大日志文件的每一行应用多个正则表达式(比如几GB长),收集非空匹配并将它们全部放入一个数组中(用于序列化并通过网络发送) 如果答案成立,则切片没有多大帮助: 如果片没有足够的容量,append将需要分配新内存并复制旧内存。对于1024个元素的切片,它将增加系数1.25 因为可以有几十万个正则表达式匹配,所以我无法真正预测一个片段的长度/容量。我也不能让它太大,“以防万一”bc这会浪费内存(或者会吗?如果内存分配器足够聪明,不会分配太多未写入的内存,也许我可以使用一个巨大的片容量而不会造成太大伤害?) 因此,我正在考虑以下备选方案:有效附加到字符串可变长度容器(Golang),go,containers,slice,Go,Containers,Slice,问题是: 我需要对一个大日志文件的每一行应用多个正则表达式(比如几GB长),收集非空匹配并将它们全部放入一个数组中(用于序列化并通过网络发送) 如果答案成立,则切片没有多大帮助: 如果片没有足够的容量,append将需要分配新内存并复制旧内存。对于1024个元素的切片,它将增加系数1.25 因为可以有几十万个正则表达式匹配,所以我无法真正预测一个片段的长度/容量。我也不能让它太大,“以防万一”bc这会浪费内存(或者会吗?如果内存分配器足够聪明,不会分配太多未写入的内存,也许我可以使用一个巨大的片
len()
work?)append()
的平均(摊销)成本已经是O(1),因为它每次都以一定的百分比增长数组。随着阵列变得越来越大,它的增长成本也越来越高,但比例上也越来越少。10M项目片的增长成本将是1M项目片的10倍,但由于我们分配的额外容量与大小成正比,因此在下次增长之前,追加(切片,项目)调用的数量也将是10x。增加的成本和减少的再分配频率相互抵消,使平均成本保持不变,即O(1)
同样的想法也适用于其他语言的动态大小数组:例如,微软的std::vector
实现显然使数组每次增长50%。摊销O(1)并不意味着您不为分配支付任何费用,只意味着您在阵列变大时继续以相同的平均费率支付
在我的笔记本电脑上,我可以在77毫秒内运行一百万个slice=append(slice,someStaticString)
s。siritinga在下面指出,它之所以快速的一个原因是“复制”字符串以放大数组实际上只是复制字符串头(指针/长度对),而不是复制内容。100000个字符串头的复制容量仍然不足2MB,与您正在处理的其他数据量相比,这不是什么大问题
container/list
对我来说,在一个微型方舟里慢了约3倍;当然,链表附加也是常数时间,但我认为append
的常数较低,因为它通常只能写入几个单词的内存,而不能分配列表项等。计时代码在操场上不起作用,但您可以在本地复制它并运行它来查看您自己:
有时,您可以有效地预分配空间以避免重新分配/复制(在本例中,使用make([]string,0,1000000)
将运行时间从~77ms缩短到~10ms),但是,当然,通常情况下,您没有足够的关于预期数据大小的信息,等等,以弥补有价值的收益,你最好把它留给内置的算法
但是您在这里提出了一个关于类
grep
的应用程序的更具体的问题(感谢您在上下文中提出了一个详细的问题)。对于这一点,最基本的建议是,如果您正在搜索GIG的日志,那么最好避免在RAM中缓冲整个输出
您可以编写一些东西将结果作为单个函数进行流式处理:logparser.Grep(in-io.Reader,out-io.Writer,patterns[]regexp.regexp)
;如果您不希望发送结果的代码与grep代码过于纠缠,您也可以将out
achan[]byte
或func(match[]byte)(err error)
(关于[]字节
与字符串
的对比:一个[]字节
似乎可以在这里完成这项工作,并且在您执行I/O时可以避免[]字节
字符串转换,所以我更喜欢这样。不过,我不知道您在做什么,如果您需要字符串
就可以了。)
如果您将整个匹配列表保留在RAM中,请注意,保留对大字符串或字节片的部分引用可以防止整个源字符串/片被垃圾收集。因此,如果你走这条路,那么你可能会反直觉地想要复制匹配项,以避免将所有源日志数据保留在RAM中。我试图将你的问题提炼成一个非常简单的例子 由于可以有“数十万个正则表达式匹配”,我为
匹配
切片容量分配了1m(1024*1024)的大量初始条目。切片是一种引用类型。在64位操作系统上,片头“struct”的长度、容量和指针总计为24(3*8)字节。因此,1M条目的片的初始分配仅为24(24*1)MB。如果有超过1M个条目,将分配一个容量为1.25M(1+1/4)条目的新切片,并将现有的1M切片头条目(24MB)复制到其中
总之,通过最初过度分配片容量,您可以避免许多append
s的开销。更大的内存问题是为每个匹配保存和引用的所有数据。更大的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
}
}
}
考虑到处理的数据量tb,我必须限制一次运行中要扫描的行数(原因应该很明显:平均I/O负载、CPU负载和驻留集大小,即防止出现大负载峰值),因此我不必真正进行流式处理。但对我来说更重要的是,我并不真正理解你的意思