【问题标题】:Go: Excessive memory usage, memory leakGo:内存使用过多,内存泄漏
【发布时间】:2014-08-07 05:53:44
【问题描述】:

我非常非常注意记忆,因为我必须编写需要处理大量数据集的程序。

目前我的应用程序很快达到 32GB 内存,开始交换,然后被系统杀死。

我不明白这是怎么回事,因为除了Trainer 结构中的TokensStructTokensCount 之外,所有变量都是可收集的(在函数中并快速释放)。 TokensCount 只是一个单位。 TokensStruct 是 [5]uint32 和字符串的 1,000,000 行切片,因此这意味着 20 个字节 + 字符串,我们可以将其称为每条记录最多 50 个字节。 50*1000000 = 需要 50MB 内存。因此,此脚本不应在函数中使用超过 50MB + 开销 + 临时可收集变量(最多可能另外 50MB。)TokensStruct 的最大潜在大小为 5,000,000,因为这是 dictionary 的大小,但即使那么它将只有 250MB 的内存。 dictionary 是一张地图,显然使用了大约 600MB 的内存,因为这就是应用程序的启动方式,但这不是问题,因为 dictionary 只加载一次,再也不会被写入。

相反,它使用 32GB 内存然后死掉。以它的速度,我预计如果可以的话,它会很高兴地达到 1TB 的内存。内存似乎随着正在加载的文件的大小呈线性增长,这意味着它似乎根本不会清除任何内存。进入应用程序的所有内容都被分配了更多内存,并且永远不会释放内存。

我尝试实现runtime.GC(),以防垃圾收集运行不够频繁,但这没有任何区别。

由于内存使用量以线性方式增加,这意味着GetTokens()LoadZip() 中存在内存泄漏。我不知道这是怎么回事,因为它们都是函数并且只执行一项任务然后关闭。或者可能是Start() 中的tokens 变量是导致泄漏的原因。基本上,看起来每个加载和解析的文件都不会从内存中释放,因为这是内存以线性方式填满并继续上升到 32GB++ 的唯一方法。

绝对的噩梦!围棋有什么问题?有什么办法解决这个问题?

package main

import (
    "bytes"
    "code.google.com/p/go.text/transform"
    "code.google.com/p/go.text/unicode/norm"
    "compress/zlib"
    "encoding/gob"
    "fmt"
    "github.com/AlasdairF/BinSearch"
    "io/ioutil"
    "os"
    "regexp"
    "runtime"
    "strings"
    "unicode"
    "unicode/utf8"
)

type TokensStruct struct {
    binsearch.Key_string
    Value [][5]uint32
}

type Trainer struct {
    Tokens      TokensStruct
    TokensCount uint
}

func checkErr(err error) {
    if err == nil {
        return
    }
    fmt.Println(`Some Error:`, err)
    panic(err)
}

// Local helper function for normalization of UTF8 strings.
func isMn(r rune) bool {
    return unicode.Is(unicode.Mn, r) // Mn: nonspacing marks
}

// This map is used by RemoveAccents function to convert non-accented characters.
var transliterations = map[rune]string{'Æ': "E", 'Ð': "D", 'Ł': "L", 'Ø': "OE", 'Þ': "Th", 'ß': "ss", 'æ': "e", 'ð': "d", 'ł': "l", 'ø': "oe", 'þ': "th", 'Œ': "OE", 'œ': "oe"}

//  removeAccentsBytes converts accented UTF8 characters into their non-accented equivalents, from a []byte.
func removeAccentsBytesDashes(b []byte) ([]byte, error) {
    mnBuf := make([]byte, len(b))
    t := transform.Chain(norm.NFD, transform.RemoveFunc(isMn), norm.NFC)
    n, _, err := t.Transform(mnBuf, b, true)
    if err != nil {
        return nil, err
    }
    mnBuf = mnBuf[:n]
    tlBuf := bytes.NewBuffer(make([]byte, 0, len(mnBuf)*2))
    for i, w := 0, 0; i < len(mnBuf); i += w {
        r, width := utf8.DecodeRune(mnBuf[i:])
        if r == '-' {
            tlBuf.WriteByte(' ')
        } else {
            if d, ok := transliterations[r]; ok {
                tlBuf.WriteString(d)
            } else {
                tlBuf.WriteRune(r)
            }
        }
        w = width
    }
    return tlBuf.Bytes(), nil
}

func LoadZip(filename string) ([]byte, error) {
    // Open file for reading
    fi, err := os.Open(filename)
    if err != nil {
        return nil, err
    }
    defer fi.Close()
    // Attach ZIP reader
    fz, err := zlib.NewReader(fi)
    if err != nil {
        return nil, err
    }
    defer fz.Close()
    // Pull
    data, err := ioutil.ReadAll(fz)
    if err != nil {
        return nil, err
    }
    return norm.NFC.Bytes(data), nil // return normalized
}

func getTokens(pibn string) []string {
    var data []byte
    var err error
    data, err = LoadZip(`/storedir/` + pibn + `/text.zip`)
    checkErr(err)
    data, err = removeAccentsBytesDashes(data)
    checkErr(err)
    data = bytes.ToLower(data)
    data = reg2.ReplaceAll(data, []byte("$2")) // remove contractions
    data = reg.ReplaceAllLiteral(data, nil)
    tokens := strings.Fields(string(data))
    return tokens
}

func (t *Trainer) Start() {
    data, err := ioutil.ReadFile(`list.txt`)
    checkErr(err)
    pibns := bytes.Fields(data)
    for i, pibn := range pibns {
        tokens := getTokens(string(pibn))
        t.addTokens(tokens)
        if i%100 == 0 {
            runtime.GC() // I added this just to try to stop the memory craziness, but it makes no difference
        }
    }
}

func (t *Trainer) addTokens(tokens []string) {
    for _, tok := range tokens {
        if _, ok := dictionary[tok]; ok {
            if indx, ok2 := t.Tokens.Find(tok); ok2 {
                ar := t.Tokens.Value[indx]
                ar[0]++
                t.Tokens.Value[indx] = ar
                t.TokensCount++
            } else {
                t.Tokens.AddKeyAt(tok, indx)
                t.Tokens.Value = append(t.Tokens.Value, [5]uint32{0, 0, 0, 0, 0})
                copy(t.Tokens.Value[indx+1:], t.Tokens.Value[indx:])
                t.Tokens.Value[indx] = [5]uint32{1, 0, 0, 0, 0}
                t.TokensCount++
            }
        }
    }
    return
}

func LoadDictionary() {
    dictionary = make(map[string]bool)
    data, err := ioutil.ReadFile(`dictionary`)
    checkErr(err)
    words := bytes.Fields(data)
    for _, word := range words {
        strword := string(word)
        dictionary[strword] = false
    }
}

var reg = regexp.MustCompile(`[^a-z0-9\s]`)
var reg2 = regexp.MustCompile(`\b(c|l|all|dall|dell|nell|sull|coll|pell|gl|agl|dagl|degl|negl|sugl|un|m|t|s|v|d|qu|n|j)'([a-z])`) //contractions
var dictionary map[string]bool

func main() {
    trainer := new(Trainer)
    LoadDictionary()
    trainer.Start()
}

【问题讨论】:

  • 你分析过它吗? dave.cheney.net/2013/07/07/…
  • 我正在对其进行分析,并将更新。
  • 好的,因此使用您链接到的漂亮包对其进行分析会导致以下语句成为内存泄漏的罪魁祸首:tokens := strings.Fields(string(data))。我已将其分成两行并再次运行以查看它是 string() 还是 strings.Fields
  • 就是这个string(data),特别是在这个里面是CALL runtime.slicebytetostring(SB)正在泄漏。
  • 在我做的另一个测试中没有泄漏,所以必须先说明我对字节所做的事情。

标签: memory-leaks go garbage-collection


【解决方案1】:

确保如果您从一个大字符串进行标记,以避免内存锁定。从上面的 cmets 中,听起来令牌是一个大字符串的子字符串。

您可能需要在 getTokens() 函数中添加一些额外内容,以确保令牌不会固定内存。

func getTokens(...) {
    // near the end of your program
    for i, t := range(tokens) {
        tokens[i] = string([]byte(t))
    }
}

顺便说一句,使用ioutil.ReadFile 一次将整个文件读入内存看起来很可疑。你确定不能使用bufio.Scanner

我正在更仔细地查看代码...如果您真的关心内存,请利用io.Reader。您应该尽量避免一次吸入整个文件的内容。使用 io.Reader 和 transform “顺其自然”。你现在使用它的方式违背了它的意图。您使用的转换包的全部意义在于构建可以流式传输数据的灵活阅读器。

例如,以下是您正在做的事情的简化:

package main

import (
    "bufio"
    "bytes"
    "fmt"
    "unicode/utf8"

    "code.google.com/p/go.text/transform"
)

type AccentsTransformer map[rune]string

func (a AccentsTransformer) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
    for nSrc < len(src) {
        // If we're at the edge, note this and return.
        if !atEOF && !utf8.FullRune(src[nSrc:]) {
            err = transform.ErrShortSrc
            return
        }
        r, width := utf8.DecodeRune(src[nSrc:])
        if r == utf8.RuneError && width == 1 {
            err = fmt.Errorf("Decoding error")
            return
        }
        if d, ok := a[r]; ok {
            if nDst+len(d) > len(dst) {
                err = transform.ErrShortDst
                return
            }
            copy(dst[nDst:], d)
            nSrc += width
            nDst += len(d)
            continue
        }

        if nDst+width > len(dst) {
            err = transform.ErrShortDst
            return
        }
        copy(dst[nDst:], src[nSrc:nSrc+width])
        nDst += width
        nSrc += width
    }
    return
}

func main() {
    transliterations := AccentsTransformer{'Æ': "E", 'Ø': "OE"}
    testString := "cØØl beÆns"
    b := transform.NewReader(bytes.NewBufferString(testString), transliterations)
    scanner := bufio.NewScanner(b)
    scanner.Split(bufio.ScanWords)
    for scanner.Scan() {
        fmt.Println("token:", scanner.Text())
    }
}

然后将转换器链接在一起变得非常容易。因此,例如,如果我们想从输入流中删除所有连字符,只需适当地使用 transform.Chain

func main() {
    transliterations := AccentsTransformer{'Æ': "E", 'Ø': "OE"}
    removeHyphens := transform.RemoveFunc(func(r rune) bool {
        return r == '-'
    })
    allTransforms := transform.Chain(transliterations, removeHyphens)

    testString := "cØØl beÆns - the next generation"
    b := transform.NewReader(bytes.NewBufferString(testString), allTransforms)
    scanner := bufio.NewScanner(b)
    scanner.Split(bufio.ScanWords)
    for scanner.Scan() {
        fmt.Println("token:", scanner.Text())
    }
}

我没有对上面的代码进行详尽的测试,所以请不要在没有充分测试的情况下复制粘贴。 :P 我只是把它煮得很快。但是这种方法——避免读取整个文件——会更好地扩展,因为它会分块读取文件。

【讨论】:

  • AccentsTransformer 现在需要实现Reset() 方法。也许,func (a AccentsTransformer) Reset() {}?无论如何,谢谢!有关如何使用 Transformer 的最清晰示例之一。
【解决方案2】:

1 “list.txt”和“dictionary”有多大?这么大,难怪内存这么大

 pibns := bytes.Fields(data)

len(pibns)多少钱?

2 启动 gc 调试 (do GODEBUG="gctrace=1" ./yourprogram ) 看看是否有任何 gc 发生

3 做一些这样的配置文件:

    func lookupMem(){
      if f, err := os.Create("mem_prof"+time.Now.Unix()); err != nil {
          log.Debug("record memory profile failed: %v", err)
      } else {
          runtime.GC()
          pprof.WriteHeapProfile(f)                                                                                                                                        
          f.Close()
      }
      if f, err := os.Create("heap_prof" + "." + timestamp); err != nil {
        log.Debug("heap profile failed:", err)
      } else {
        p := pprof.Lookup("heap")
        p.WriteTo(f, 2)
      }
   }

    func (t *Trainer) Start() {      
    .......
      if i%1000==0 {
        //if `len(pibns)` is not very large , record some meminfo
        lookupMem()
      }
    .......

【讨论】:

  • dictionary 包含 500 万字,应用启动时占用 600MB,因为它存储在地图中并且地图有很多开销,但这并没有增加,因为它随后被用作只读. pibns 包含 300,000 个 10 位数字的列表,因此大小为 3MB。这些都不能使用任何接近 32GB 的内存。我会检查个人资料并更新,谢谢。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-09-19
  • 2020-04-04
  • 2012-10-19
  • 2012-05-05
  • 1970-01-01
相关资源
最近更新 更多