该程序应该读取由一对整数组成的文件(每行一对)并删除重复对。虽然它适用于小文件,但它会对大文件(例如1.5 GB的文件)引发运行时错误。最初,我认为是导致这种情况的地图数据结构,但即使在评论之后,它仍然会耗尽内存。任何想法为什么会这样?如何纠正呢?这是一个内存不足的数据文件:http://snap.stanford.edu/data/com-Orkut.html
package main
import (
"fmt"
"bufio"
"os"
"strings"
"strconv"
)
func main() {
file, err := os.Open(os.Args[1])
if err != nil {
panic(err.Error())
}
defer file.Close()
type Edge struct {
u, v int
}
//seen := make(map[Edge]bool)
edges := []Edge{}
scanner := bufio.NewScanner(file)
for i, _ := strconv.Atoi(os.Args[2]); i > 0; i-- {
scanner.Scan()
}
for scanner.Scan() {
str := scanner.Text()
edge := strings.Split(str, ",")
u, _ := strconv.Atoi(edge[0])
v, _ := strconv.Atoi(edge[1])
var key Edge
if u < v {
key = Edge{u,v}
} else {
key = Edge{v,u}
}
//if seen[key] {
// continue
//}
//seen[key] = true
edges = append(edges, key)
}
for _, e := range edges {
s := strconv.Itoa(e.u) + "," + strconv.Itoa(e.v)
fmt.Println(s)
}
}
下面给出了一个示例输入。该程序可以如下运行(最后一个输入表示要跳过多少行)。 go run undup.go a.txt 1
# 3072441,117185083
1,2
1,3
1,4
1,5
1,6
1,7
1,8
答案 0 :(得分:5)
我查看了这个文件:com-orkut.ungraph.txt
,它包含117,185,082行。数据的结构方式,每行至少16个字节。 (Edge是两个64位整数)仅此为1.7GB。我过去遇到过这个问题,这可能是一个棘手的问题。您是否尝试针对特定用例(相关文件)或一般情况解决此问题?
在特定情况下,您可以利用的数据有一些:(1)按键排序;(2)它看起来每次连接存储两次,(3)数字看起来不是很大。以下是一些想法:
如果您使用较小的类型键,您将使用较少的内存。试试uint32
。
您可以通过简单地查看第二列是否大于第一列来流式传输(不使用地图)其他文件的键:
if u < v {
// write the key to another file
} else {
// skip it because v will eventually show v -> u
}
对于一般情况,您可以使用几种策略:
如果结果列表的顺序无关紧要:使用磁盘上的哈希表来存储地图。有很多这样的:leveldb,sqlite,tokyo tyrant,......一个非常好的go for bolt。
在for循环中,您只需检查一个存储桶是否包含给定的密钥。 (您可以使用编码/二进制将整数转换为字节切片)如果是,则跳过它并继续。您需要将第二个for循环处理步骤移动到第一个for循环中,这样您就不必存储所有键。
如果结果列表的顺序很重要(并且您无法保证输入的顺序):您还可以使用磁盘上的哈希表,但需要对其进行排序。螺栓经过排序,以便工作。将所有键添加到其中,然后在第二个循环中遍历它。
以下是一个示例:(此程序需要一段时间才能运行1亿条记录)
package main
import (
"bufio"
"encoding/binary"
"fmt"
"github.com/boltdb/bolt"
"os"
"strconv"
"strings"
)
type Edge struct {
u, v int
}
func FromKey(bs []byte) Edge {
return Edge{int(binary.BigEndian.Uint64(bs[:8])), int(binary.BigEndian.Uint64(bs[8:]))}
}
func (e Edge) Key() [16]byte {
var k [16]byte
binary.BigEndian.PutUint64(k[:8], uint64(e.u))
binary.BigEndian.PutUint64(k[8:], uint64(e.v))
return k
}
func main() {
file, err := os.Open(os.Args[1])
if err != nil {
panic(err.Error())
}
defer file.Close()
scanner := bufio.NewScanner(file)
for i, _ := strconv.Atoi(os.Args[2]); i > 0; i-- {
scanner.Scan()
}
db, _ := bolt.Open("ex.db", 0777, nil)
defer db.Close()
bucketName := []byte("edges")
db.Update(func(tx *bolt.Tx) error {
tx.CreateBucketIfNotExists(bucketName)
return nil
})
batchSize := 10000
total := 0
batch := make([]Edge, 0, batchSize)
writeBatch := func() {
total += len(batch)
fmt.Println("write batch. total:", total)
db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket(bucketName)
for _, edge := range batch {
key := edge.Key()
bucket.Put(key[:], nil)
}
return nil
})
}
for scanner.Scan() {
str := scanner.Text()
edge := strings.Split(str, "\t")
u, _ := strconv.Atoi(edge[0])
v, _ := strconv.Atoi(edge[1])
var key Edge
if u < v {
key = Edge{u, v}
} else {
key = Edge{v, u}
}
batch = append(batch, key)
if len(batch) == batchSize {
writeBatch()
// reset the batch length to 0
batch = batch[:0]
}
}
// write anything leftover
writeBatch()
db.View(func(tx *bolt.Tx) error {
tx.Bucket(bucketName).ForEach(func(k, v []byte) error {
edge := FromKey(k)
fmt.Println(edge)
return nil
})
return nil
})
}
答案 1 :(得分:2)
你在浪费内存。以下是如何纠正它。
您提供示例输入a.txt
,48个字节。
# 3072441,117185083
1,2
1,3
1,4
1,5
在http://snap.stanford.edu/data/com-Orkut.html上,我找到http://snap.stanford.edu/data/bigdata/communities/com-orkut.ungraph.txt.gz,1.8 GB未压缩,117,185,083边。
# Undirected graph: ../../data/output/orkut.txt
# Orkut
# Nodes: 3072441 Edges: 117185083
# FromNodeId ToNodeId
1 2
1 3
1 4
1 5
在http://socialnetworks.mpi-sws.org/data-imc2007.html上,我找到http://socialnetworks.mpi-sws.mpg.de/data/orkut-links.txt.gz,未压缩的3.4 GB,边缘为223,534,301。
1 2
1 3
1 4
1 5
由于它们相似,一个程序可以处理所有格式。
您的Edge
类型
type Edge struct {
u, v int
}
在64位架构上是16个字节。
使用
type Edge struct {
U, V uint32
}
这是8个字节,足够了。
如果切片的容量不足以容纳其他值,append
会分配一个足够大的新基础数组,该数组既适合现有切片元素又适合其他值。否则,append
重新使用底层数组。对于大切片,新阵列的大小是旧阵列的1.25倍。当旧数组被复制到新数组时,需要1 + 1.25 =旧数组内存的2.25倍。因此,分配底层数组以使所有值都适合。
make(T, n)
使用n个元素的初始空间初始化类型T的映射。为n添加一个值,以便在添加元素时限制重组和碎片的成本。散列函数通常是不完美的,这会导致浪费的空间。消除地图,因为它是不必要的。要消除重复,请对切片进行排序并向下移动唯一元素。
string
是不可变的,因此为string
分配了一个新scanner.Text()
来转换byte
切片缓冲区。要解析数字,我们使用strconv
。要最小化临时分配,请使用scanner.Bytes()
并调整strconv.ParseUint
以接受字节数组参数(bytconv
)。
例如,
orkut.go
package main
import (
"bufio"
"bytes"
"errors"
"fmt"
"os"
"runtime"
"sort"
"strconv"
)
type Edge struct {
U, V uint32
}
func (e Edge) String() string {
return fmt.Sprintf("%d,%d", e.U, e.V)
}
type ByKey []Edge
func (a ByKey) Len() int { return len(a) }
func (a ByKey) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByKey) Less(i, j int) bool {
if a[i].U < a[j].U {
return true
}
if a[i].U == a[j].U && a[i].V < a[j].V {
return true
}
return false
}
func countEdges(scanner *bufio.Scanner) int {
var nNodes, nEdges int
for scanner.Scan() {
line := scanner.Bytes()
if !(len(line) > 0 && line[0] == '#') {
nEdges++
continue
}
n, err := fmt.Sscanf(string(line), "# Nodes: %d Edges: %d", &nNodes, &nEdges)
if err != nil || n != 2 {
n, err = fmt.Sscanf(string(line), "# %d,%d", &nNodes, &nEdges)
if err != nil || n != 2 {
continue
}
}
fmt.Println(string(line))
break
}
if err := scanner.Err(); err != nil {
panic(err.Error())
}
fmt.Println(nEdges)
return nEdges
}
func loadEdges(filename string) []Edge {
file, err := os.Open(filename)
if err != nil {
panic(err.Error())
}
defer file.Close()
scanner := bufio.NewScanner(file)
nEdges := countEdges(scanner)
edges := make([]Edge, 0, nEdges)
offset, err := file.Seek(0, os.SEEK_SET)
if err != nil || offset != 0 {
panic(err.Error())
}
var sep byte = '\t'
scanner = bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Bytes()
if len(line) > 0 && line[0] == '#' {
continue
}
i := bytes.IndexByte(line, sep)
if i < 0 || i+1 >= len(line) {
sep = ','
i = bytes.IndexByte(line, sep)
if i < 0 || i+1 >= len(line) {
err := errors.New("Invalid line format: " + string(line))
panic(err.Error())
}
}
u, err := ParseUint(line[:i], 10, 32)
if err != nil {
panic(err.Error())
}
v, err := ParseUint(line[i+1:], 10, 32)
if err != nil {
panic(err.Error())
}
if u > v {
u, v = v, u
}
edges = append(edges, Edge{uint32(u), uint32(v)})
}
if err := scanner.Err(); err != nil {
panic(err.Error())
}
if len(edges) <= 1 {
return edges
}
sort.Sort(ByKey(edges))
j := 0
i := j + 1
for ; i < len(edges); i, j = i+1, j+1 {
if edges[i] == edges[j] {
break
}
}
for ; i < len(edges); i++ {
if edges[i] != edges[j] {
j++
edges[j] = edges[i]
}
}
edges = edges[:j+1]
return edges
}
func main() {
if len(os.Args) <= 1 {
err := errors.New("Missing file name")
panic(err.Error())
}
filename := os.Args[1]
fmt.Println(filename)
edges := loadEdges(filename)
var ms runtime.MemStats
runtime.ReadMemStats(&ms)
fmt.Println(ms.Alloc, ms.TotalAlloc, ms.Sys, ms.Mallocs, ms.Frees)
fmt.Println(len(edges), cap(edges))
for i, e := range edges {
fmt.Println(e)
if i >= 10 {
break
}
}
}
// bytconv from strconv
// Return the first number n such that n*base >= 1<<64.
func cutoff64(base int) uint64 {
if base < 2 {
return 0
}
return (1<<64-1)/uint64(base) + 1
}
// ParseUint is like ParseInt but for unsigned numbers.
func ParseUint(s []byte, base int, bitSize int) (n uint64, err error) {
var cutoff, maxVal uint64
if bitSize == 0 {
bitSize = int(strconv.IntSize)
}
s0 := s
switch {
case len(s) < 1:
err = strconv.ErrSyntax
goto Error
case 2 <= base && base <= 36:
// valid base; nothing to do
case base == 0:
// Look for octal, hex prefix.
switch {
case s[0] == '0' && len(s) > 1 && (s[1] == 'x' || s[1] == 'X'):
base = 16
s = s[2:]
if len(s) < 1 {
err = strconv.ErrSyntax
goto Error
}
case s[0] == '0':
base = 8
default:
base = 10
}
default:
err = errors.New("invalid base " + strconv.Itoa(base))
goto Error
}
n = 0
cutoff = cutoff64(base)
maxVal = 1<<uint(bitSize) - 1
for i := 0; i < len(s); i++ {
var v byte
d := s[i]
switch {
case '0' <= d && d <= '9':
v = d - '0'
case 'a' <= d && d <= 'z':
v = d - 'a' + 10
case 'A' <= d && d <= 'Z':
v = d - 'A' + 10
default:
n = 0
err = strconv.ErrSyntax
goto Error
}
if int(v) >= base {
n = 0
err = strconv.ErrSyntax
goto Error
}
if n >= cutoff {
// n*base overflows
n = 1<<64 - 1
err = strconv.ErrRange
goto Error
}
n *= uint64(base)
n1 := n + uint64(v)
if n1 < n || n1 > maxVal {
// n+v overflows
n = 1<<64 - 1
err = strconv.ErrRange
goto Error
}
n = n1
}
return n, nil
Error:
return n, &strconv.NumError{"ParseUint", string(s0), err}
}
输出:
$ go build orkut.go
$ time ./orkut ~/release-orkut-links.txt
/home/peter/release-orkut-links.txt
223534301
1788305680 1788327856 1904683256 135 50
117185083 223534301
1,2
1,3
1,4
1,5
1,6
1,7
1,8
1,9
1,10
1,11
1,12
real 2m53.203s
user 2m51.584s
sys 0m1.628s
$
带有orkut.go
文件的release-orkut-links.txt
程序(3,372,855,860(3.4 GB)字节,边长为223,534,301)使用大约1.8 GiB的内存。消除重复后,剩余117,185,083个独特边缘。这匹配117,185,083唯一边com-orkut.ungraph.txt
文件。
您的计算机上有8 GB的内存,您可以加载更大的文件。