为什么以下golang程序会导致运行时出现内存错误?

时间:2014-10-12 09:48:15

标签: go out-of-memory runtime-error

该程序应该读取由一对整数组成的文件(每行一对)并删除重复对。虽然它适用于小文件,但它会对大文件(例如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

2 个答案:

答案 0 :(得分:5)

我查看了这个文件:com-orkut.ungraph.txt,它包含117,185,082行。数据的结构方式,每行至少16个字节。 (Edge是两个64位整数)仅此为1.7GB。我过去遇到过这个问题,这可能是一个棘手的问题。您是否尝试针对特定用例(相关文件)或一般情况解决此问题?

在特定情况下,您可以利用的数据有一些:(1)按键排序;(2)它看起来每次连接存储两次,(3)数字看起来不是很大。以下是一些想法:

  1. 如果您使用较小的类型键,您将使用较少的内存。试试uint32

  2. 您可以通过简单地查看第二列是否大于第一列来流式传输(不使用地图)其他文件的键:

    if u < v {
        // write the key to another file
    } else {
        // skip it because v will eventually show v -> u
    }
    
  3. 对于一般情况,您可以使用几种策略:

    1. 如果结果列表的顺序无关紧要:使用磁盘上的哈希表来存储地图。有很多这样的:leveldb,sqlite,tokyo tyrant,......一个非常好的go for bolt

      在for循环中,您只需检查一个存储桶是否包含给定的密钥。 (您可以使用编码/二进制将整数转换为字节切片)如果是,则跳过它并继续。您需要将第二个for循环处理步骤移动到第一个for循环中,这样您就不必存储所有键。

    2. 如果结果列表的顺序很重要(并且您无法保证输入的顺序):您还可以使用磁盘上的哈希表,但需要对其进行排序。螺栓经过排序,以便工作。将所有键添加到其中,然后在第二个循环中遍历它。

    3. 以下是一个示例:(此程序需要一段时间才能运行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的内存,您可以加载更大的文件。