C++的 set 爆内存,求助

2017-10-25 09:42:17 +08:00
 scenix

如题,本来想写个小程序,对比一下 c++,python,golang 处理日志文件的效率。找了个 600MB 的日志文件试水,结果因为本人水平有限,导致 c++的实现出现了大量的内存消耗,运行不完就被 kill 了。

程序的思路是逐行读取日志文件,用空格切分,第一个字段作为 key,剩下的字段去重后作为 value。

先贴一下 python 的实现

big_dict = {}
with open("data/test_log.txt") as f_in:
    for line in f_in:
        items = line.strip().split(' ')
        key = items[0]
        if key not in big_dict:
            big_dict[key] = set([])
        for i in items[1:]:
            big_dict[key].add(i)

print "total keys:", len(big_dict)

再贴一下 golang 的:

package main

import (
    "bufio"
    "fmt"
    "io"
    "os"
    "strings"
)


func process(fname string, big_dict map[string]map[string]int) {
    fin, err := os.Open(fname)
    defer fin.Close()
    if err != nil {
        fmt.Println("Open file error ", err)
        return
    }

    buf := bufio.NewReader(fin)
    line_count := 0
    for ; ; line_count++ {
        line, err := buf.ReadString('\n')
        if err != nil {
            if io.EOF == err {
                break
            }
            fmt.Println("Error in ReadBytes: ", err)
            return
        }
        items := strings.Split(line, " ")
        key := items[0]

        elem, ok := big_dict[key]
        if false == ok {
            big_dict[key] = make(map[string]int)
        }
        elem = big_dict[key]
        for i := 1; i < len(items); i++ {
            elem[items[i]] = 1
        }
    }
    fmt.Println("Total Line Count: ", line_count)
}

func main() {
    const fname = "data/test_log.txt"
    big_dict := make(map[string]map[string]int)
    process(fname, big_dict)
    fmt.Println("Total Key Count: ", len(big_dict))
}

最后贴一下 c++的。

#include <iostream>
#include <fstream>
#include <string>
#include<unordered_map>
#include<unordered_set>

using namespace std;

// data/test_log.txt 文件是一个 600MB 的日志文件
const string IN_FNAME = "data/test_log.txt";
unordered_map<string, unordered_set<string *>* > big_dict;

void process_file(const string fname, unordered_map<string,unordered_set<string*> *> & big_dict) {
    ifstream f_in;
    f_in.open(fname, ios::in);
    string line = "";
    int total_line = 0;
    size_t start =0, end = 0;
    while(getline(f_in, line)) {
        ++total_line;
        start =0, end = 0;// c++没有内建 string 的分割,自己 diy 个
        end = line.find_first_of(' ',start);
        string key = line.substr(start,end);
        // 寻找是否存在 key
        if(big_dict.find(key) == big_dict.end()) {
            unordered_set<string*> *p = new unordered_set<string*>;
            big_dict[key] = p;
        }

        start = end+1;
        while(start<line.size()) {
            end = line.find_first_of(' ',start);
            if(string::npos == end) end = line.size() - 1;
            string value = line.substr(start,end);
            big_dict[key]->insert(&value);//大部分的时间都在这个 insert 上了
            //这里我存的是 string 的指针,实际上无法得到去重的效果
            //但是我如果直接存 string 对象,会直接爆内存
            start = end + 1;
        }
    }
    f_in.close();

}

int main() {

    process_file(IN_FNAME, big_dict);

    cout<<"Total Keys:"<<big_dict.size()<<endl;

    return 0;
}

c++的实现中,big_dict[key]->insert(&value);大部分的时间都在这个 insert 上了,这里我存的是 string 的指针,实际上无法得到去重的效果。但是我如果直接存 string 对象,会直接爆内存。我存指针可以解决内存的问题,但是执行速度上依然是 go 快过 python,最慢的是 c++。

希望有大神能指点下,看我的 c++代码哪里出了问题。

3696 次点击
所在节点    C
48 条回复
lcdtyph
2017-10-25 09:53:28 +08:00
临时变量取指针存储就不提了,两个相等它们的地址就像等吗?用地址做 key 你起码要重新定义 comp 函数啊。
scenix
2017-10-25 10:06:36 +08:00
@lcdtyph 首先感谢你的回复,我自然知道这段代码是实际上无法得到去重的效果的,我文中和代码注释中已经做了说明。我现在想解决的是运行的慢的问题,我重新定义 comp 函数能不能把速度提上来?请赐教。
03
2017-10-25 10:06:47 +08:00
string value = line.substr(start,end); 这里

substr 定义是 basic_string substr( size_type pos = 0,
size_type count = npos ) const;

另外你这 C++到处指针看着挺不舒服的,临时变量存指针的问题楼上已经提过就不多说了
scenix
2017-10-25 10:12:50 +08:00
@03 感谢你的回复,我自然知道这段代码是实际上无法得到去重的效果的,我文中和代码注释中已经做了说明。原来 key 是直接存储的 string 对象,之所以存成指针是想着试试看光存指针会不会速度上来,结果这样改了下是不爆内存,但还是慢,就没心思管什么 const 不 const, 存成指针后里面东西还能不能用的问题了。
jmc891205
2017-10-25 10:18:02 +08:00
template < class Key, // unordered_set::key_type/value_type
_________ class Hash = hash<Key>, // unordered_set::hasher
_________ class Pred = equal_to<Key>, // unordered_set::key_equal
_________ class Alloc = allocator<Key> // unordered_set::allocator_type
________> class unordered_set;

第二个或第三个模版参数自己传一个进去
sagaxu
2017-10-25 10:21:56 +08:00
@scenix 你这不是不能去重的问题,是指针已经飞了,完全错误了
jmc891205
2017-10-25 10:22:49 +08:00
@scenix 如果你的 key 重复的很多的话 那去重之后速度应该会快很多。因为现在你是每一次 insert 都要在所有的 key 里做一遍 search。
scenix
2017-10-25 10:28:09 +08:00
@jmc891205 @sagaxu 感谢二位回复,所以现在看来我存指针是完全错误的方向了。那么我直接存 string 对象作为 key,内存飞涨的问题怎么解决呢? 只有 600M 的文件读入,python 大概需要 800M 内存的样子,go 使用的更少。
目前我定义为
```c++
unordered_map<string, unordered_set<string>* > big_dict;`
```

存的时候
```c++
big_dict[key]->insert(line.substr(start, end)); //这样内存吃不消
```
acros
2017-10-25 10:28:45 +08:00
楼上的意思是,你存错了 string 指针,value 是 substr 得到的栈内存地址····
lcdtyph
2017-10-25 10:38:44 +08:00
@scenix 你肯定是这种写法也取了临时 set 的指针,导致下次插入时候那块内存已经失效了。不是内存不够爆了。
acros
2017-10-25 10:38:52 +08:00
600m 文件似乎还不至爆内存,不是上面内存访问错误吧。
lcdtyph
2017-10-25 10:48:57 +08:00
@lcdtyph 看错了代码,不是这个原因。我还是等有电脑了再看吧…
arakashic
2017-10-25 10:52:22 +08:00
```
big_dict = {}
with open("data/test_log.txt") as f_in:
for line in f_in:
items = line.strip().split(' ')
key = items[0]
if key not in big_dict:
big_dict[key] = set([])
#下面这两行有何意义?又不影响 len(big_dict)
for i in items[1:]:
big_dict[key].add(i)

print "total keys:", len(big_dict)
```
scenix
2017-10-25 10:53:12 +08:00
@lcdtyph @acros 我运行的时候一直用 top 命令看内存,眼看着内存使用一路飙到 3G,然后被 kill,没有异常,没有 core,直接 kill 的。
我的 set 存的时候是这么写的:

auto *p = new unordered_set<string>;
big_dict[key] = p;

可以看到只 new 了没有 delete。但是我如果不在这个 new 的 set 里面存东西的话,也不会爆内存,大量的时间和内存都是在下面这句消耗的:

big_dict[key]->insert(line.substr(start, end));
scenix
2017-10-25 11:00:06 +08:00
@arakashic 感谢回复,程序的思路是逐行读取日志文件,用空格切分,第一个字段作为 key,剩下的字段去重后作为 value。只是为了看下处理效率,当然接着写的话,可以把 big_dict 里面的东西每个(key,value)作为一行输出到一个文件里吧。
hncqp
2017-10-25 11:01:29 +08:00
@scenix
line.substr(start,end); 修改为 line.substr(start,end - start);
set 存 string,不要存临时地址
big_dict[key]->insert(&value); 修改为迭代器访问,不要每次都去 find
williamscorn
2017-10-25 11:03:59 +08:00
#include <iostream>
#include <fstream>
#include <sstream>
#include <map>
#include <set>
using namespace std;

const string IN_FNAME = "data/test_log.txt";
map<string,set<string> >dict;

int main() {
ios::sync_with_stdio(false);
fstream rin(IN_FNAME);
rin.tie(0);

string buf,key,val;
stringstream ss;
while(getline(rin,buf)){
set<string>tmp;
ss.str(buf);//format:key val_1 val_2 ... val_n
ss>>key;
while(!ss.eof()){
ss>>val;
tmp.insert(val);
}
dict.insert(make_pair(key,tmp));
}
cout<<"Total Keys:"<<dict.size()<<'\n';
}
Gingko
2017-10-25 11:12:04 +08:00
@williamscorn 正解
williamscorn
2017-10-25 11:13:37 +08:00
@Gingko
少打了两个头文件...
bits/stdc++.h 用多了...
clang 居然还能正确编译...
#include <utility>
#include <string>
wevsty
2017-10-25 11:24:56 +08:00
这个 cpp 写的。。。
line.substr(start,end)这里的问题前面已经有人指出来了,end 不应该是结束为止的标号,而是复制的长度。
同理 end = line.size() - 1;一样是有问题的。
unordered_map<string, unordered_set<string>* > big_dict;
这个定义是一个 key 指向一个不会重复的 string 指针,big_dict[key]->insert(&value);实际是插入了 value 这个 string 的指针,然而 value 在循环结束的时候就因为生存周期结束而销毁了,所以你才觉得这样不会爆内存。

不要用那么多指针来掩饰,你程序里想表达的数据结构实际上就是:
unordered_map<string,unordered_set<string> >
在 map 里套 set,那么 key 至少存在 2 次,存在重复查找,重复存放一堆问题,效率能高才怪了
比如日志中某一行是
“ key value1 value2 ”那么运行完成以后数据结构实际上是
{'key':{'key':'value2'}}
而你的 python 代码对应的结果应该是
{'key':['value1','value2']}
从结果上看,不要谈效率,代码的实现完全都是不对的。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/400452

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX