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++代码哪里出了问题。

4334 次点击
所在节点    C
48 条回复
wevsty
2017-10-25 11:33:17 +08:00
@williamscorn 按照我的理解,从楼主的意图来说,也许不应该用 std::set,而是用 std::list 或者 std::vector 更合适一些。
williamscorn
2017-10-25 11:35:05 +08:00
@wevsty
是的,vector 存好,sort 之后 unique+erase
我只是写了最直接的版本,给楼主留了很多的优化空间
scenix
2017-10-25 12:16:42 +08:00
@wevsty @williamscorn @hncqp 非常感谢提点。爆内存的问题的确是我错误的使用了 substr,修改为 line.substr(start,end - start)后,的确内存占用很少了,大概只有 200M。但是执行的速度还是比不上 python 和 go,不管是我原来的代码,还是 @williamscorn 提供的代码,大概都要慢一倍的样子。

P.S. @williamscorn 提供的代码中我每个循环加了一句 ss.clear(); 其余没有动。
wevsty
2017-10-25 12:29:23 +08:00
@scenix 速度的问题,上测试用例。对于测试用例 @williamscorn 的代码也未必就是速度最优的选择,剩下的具体问题具体分析。
scenix
2017-10-25 12:33:09 +08:00
@wevsty 大概就是这样一个日志文件:

$ head data/test_log.txt
03:07:02 giantbee systemd: Removed slice user-0.slice. Sep 25 03:07:02 giantbee systemd: Removed slice user-0.slice.
03:07:02 giantbee systemd: Stopping user-0.slice. Sep 25 03:07:02 giantbee systemd: Stopping user-0.slice.
03:07:02 giantbee kibana: "type":"log" "@timestamp":"2017-09-24T19:07:02Z" "tags":["warning" "elasticsearch" "admin"] "pid":22721 "message":"Unable to revive connection: http://localhost:9200/" Sep 25 03:07:02 giantbee kibana: {"type":"log","@timestamp":"2017-09-24T19:07:02Z","tags":["warning","elasticsearch","admin"],"pid":22721,"message":"Unable to revive connection: http://localhost:9200/"}
03:07:02 giantbee kibana: "type":"log" "@timestamp":"2017-09-24T19:07:02Z" "tags":["warning" "elasticsearch" "admin"] "pid":22721 "message":"No living connections" Sep 25 03:07:02 giantbee kibana: {"type":"log","@timestamp":"2017-09-24T19:07:02Z","tags":["warning","elasticsearch","admin"],"pid":22721,"message":"No living connections"}
03:07:05 giantbee kibana: "type":"log" "@timestamp":"2017-09-24T19:07:05Z" "tags":["warning" "elasticsearch" "admin"] "pid":22721 "message":"Unable to revive connection: http://localhost:9200/" Sep 25 03:07:05 giantbee kibana: {"type":"log","@timestamp":"2017-09-24T19:07:05Z","tags":["warning","elasticsearch","admin"],"pid":22721,"message":"Unable to revive connection: http://localhost:9200/"}
03:07:05 giantbee kibana: "type":"log" "@timestamp":"2017-09-24T19:07:05Z" "tags":["warning" "elasticsearch" "admin"] "pid":22721 "message":"No living connections" Sep 25 03:07:05 giantbee kibana: {"type":"log","@timestamp":"2017-09-24T19:07:05Z","tags":["warning","elasticsearch","admin"],"pid":22721,"message":"No living connections"}
03:07:07 giantbee kibana: "type":"log" "@timestamp":"2017-09-24T19:07:07Z" "tags":["warning" "elasticsearch" "admin"] "pid":22721 "message":"Unable to revive connection: http://localhost:9200/" Sep 25 03:07:07 giantbee kibana: {"type":"log","@timestamp":"2017-09-24T19:07:07Z","tags":["warning","elasticsearch","admin"],"pid":22721,"message":"Unable to revive connection: http://localhost:9200/"}
03:07:07 giantbee kibana: "type":"log" "@timestamp":"2017-09-24T19:07:07Z" "tags":["warning" "elasticsearch" "admin"] "pid":22721 "message":"No living connections" Sep 25 03:07:07 giantbee kibana: {"type":"log","@timestamp":"2017-09-24T19:07:07Z","tags":["warning","elasticsearch","admin"],"pid":22721,"message":"No living connections"}
03:07:10 giantbee kibana: "type":"log" "@timestamp":"2017-09-24T19:07:10Z" "tags":["warning" "elasticsearch" "admin"] "pid":22721 "message":"Unable to revive connection: http://localhost:9200/" Sep 25 03:07:10 giantbee kibana: {"type":"log","@timestamp":"2017-09-24T19:07:10Z","tags":["warning","elasticsearch","admin"],"pid":22721,"message":"Unable to revive connection: http://localhost:9200/"}
03:07:10 giantbee kibana: "type":"log" "@timestamp":"2017-09-24T19:07:10Z" "tags":["warning" "elasticsearch" "admin"] "pid":22721 "message":"No living connections" Sep 25 03:07:10 giantbee kibana: {"type":"log","@timestamp":"2017-09-24T19:07:10Z","tags":["warning","elasticsearch","admin"],"pid":22721,"message":"No living connections"}


另外 @williamscorn 的代码中 sstream 的>>操作是不是不等价于 python 和 go 的 split,这次是空格,下次我要用\t 或者是|来分割字符串的时候是不是 sstream 就不好用了啊?
pezy
2017-10-25 13:58:51 +08:00
插一句,你的 cpp 编译命令是怎样的?运行环境是什么?
whatTheGhost
2017-10-25 14:26:30 +08:00
@williamscorn set 和 string 都被深拷贝了吧。
scenix
2017-10-25 14:36:57 +08:00
@pezy centos7 用的 cmake 开了-O3 优化
wevsty
2017-10-25 15:50:52 +08:00
stringstream 的实现好像确实不是很快,所以我改了一下。
修改以后在 xubuntu 下面跑了一下,测试的数据是用图里 python 里写的 write_test_file 这个函数生成的。
[img]https://i.loli.net/2017/10/25/59f03fd257b2c.png[/img]
在 O2 优化下程序的时间是 0.267 秒,O3 优化的话和 O2 差不太多,python 下面则是 0.477 秒。
下一楼贴代码
wevsty
2017-10-25 15:51:16 +08:00
#include <iostream>
#include <fstream>
#include <sstream>
#include <map>
#include <set>
#include <vector>
#include <string>
#include <time.h>
#include <unordered_map>
#include <unordered_set>
using namespace std;

const string IN_FNAME = "/home/xubuntu/v2ex/test/test_py.txt";
unordered_map<string, unordered_set<string> > dict;

//字符串分割
std::vector<string> string_split(const string &str_data,
const string &str_separator
, string::size_type n_max_split_count = (string::size_type) - 1)
{
std::vector<string> vec_split_str;
string str_subtmp;
string::size_type n_datalen = str_data.length();
string::size_type n_separator = str_separator.length();
string::size_type n_start = 0;
string::size_type n_index = 0;
if (n_max_split_count == 0)
{
return vec_split_str;
}
do
{
if (n_max_split_count == 0)
{
n_max_split_count--;
break;
}
n_index = str_data.find(str_separator, n_start);
if (n_index != string::npos)
{
str_subtmp = str_data.substr(n_start, n_index - n_start);
if (str_subtmp.length() != 0)
{
vec_split_str.push_back(str_subtmp);
}
n_start = n_index + n_separator;
if (n_start >= n_datalen)
{
break;
}
}
else
{
str_subtmp = str_data.substr(n_start);
if (str_subtmp.length() != 0)
{
vec_split_str.push_back(str_subtmp);
}
break;
}
} while (n_index != string::npos);
return std::move(vec_split_str);
}

void read_message(const string& filepath)
{
fstream rin(filepath);
string buf;
string sep(" ");
std::vector<string> vec_list;
vec_list.reserve(20);
while (getline(rin, buf)) {
vec_list = string_split(buf, sep);
for (size_t i = 1; i < vec_list.size(); i++)
{
dict[vec_list[0]].insert(vec_list[i]);
}
vec_list.clear();
}
}

int main()
{
clock_t start = clock();
read_message(IN_FNAME);
clock_t end = clock();
cout << "time: " << ((double)end - (double)start) / CLOCKS_PER_SEC << " sec" << '\n';
cout << "Total Keys:" << dict.size() << '\n';
return 0;
}
arzterk
2017-10-25 16:01:04 +08:00
可以用 C++14 的右值进一步提速。
或者直接用 multi_hashmap 做一次全插入,然后在去重。
pezy
2017-10-25 16:01:54 +08:00
加上 std::ios::sync_with_stdio(false); 这句话,试试呢
acgnsstech
2017-10-25 17:24:57 +08:00
把 map 改成 vector 效率还能提升一倍!
mainjzb
2017-10-25 17:31:56 +08:00
pezy 说的对 加上 std::ios::sync_with_stdio(false); cout 的输出效率是有问题的。或者换成 printf 试一下
wevsty
2017-10-25 17:33:29 +08:00
@wevsty
更正一下,字符串分割函数的 max_split 功能一不小心写错了,不过不影响这个例子中的运行结果。
//字符串分割
std::vector<string> string_split(const string &str_data,
const string &str_separator
, string::size_type n_max_split_count = (string::size_type) - 1)
{
std::vector<string> vec_split_str;
string str_subtmp;
string::size_type n_datalen = str_data.length();
string::size_type n_separator = str_separator.length();
string::size_type n_start = 0;
string::size_type n_index = 0;
do
{
if (n_max_split_count == 0)
{
break;
}
n_max_split_count--;
n_index = str_data.find(str_separator, n_start);
if (n_index != string::npos)
{
str_subtmp = str_data.substr(n_start, n_index - n_start);
if (str_subtmp.length() != 0)
{
vec_split_str.push_back(str_subtmp);
}
n_start = n_index + n_separator;
if (n_start >= n_datalen)
{
break;
}
}
else
{
str_subtmp = str_data.substr(n_start);
if (str_subtmp.length() != 0)
{
vec_split_str.push_back(str_subtmp);
}
break;
}
} while (n_index != string::npos);
return vec_split_str;
}
billwsy
2017-10-25 23:28:13 +08:00
xziar
2017-10-26 00:42:27 +08:00
big_dict[key]->insert(value);
就这个,每次都要在 big_dict 里查找一边 key。虽然是 O(1)但用多了也会影响速度(并不确定有多大影响)。
这个写法就非常不 C++。之前 find 的时候就该缓存一下查找结果。
具体的你可以贴出最终代码,vs 或者 vtune 什么的都可以跑 profile 找程序热点。
字符串分割的话 boost 之类的库都有,我也自己写过一个简单的(导出 string_view 避免拷贝的)

/**
** @brief split source using judger, putting slice into container
** @param src source
** @param judger a function that accepts one element and return (bool) whether it is delim
** @param container a container that allows push_back to put a slice of std::basic_string_view<CharT>
** @param keepblank whether should keep blank splice
**/
template<class T, class CharT = T::value_type, class Judger, class Container>
inline void split(const T& src, const Judger judger, Container& container, const bool keepblank = true)
{
using namespace std;
size_t cur = 0, last = 0, len = src.length();
for (; cur < len; cur++)
{
if (judger(src[cur]))
{
if (keepblank || cur != last)
container.push_back(basic_string_view<CharT>(&src[last], cur - last));
last = cur + 1;
}
}
if (keepblank || cur != last)
container.push_back(basic_string_view<CharT>(&src[last], cur - last));
}
gnaggnoyil
2017-10-26 10:27:06 +08:00
用 string_view 减少不必要的字符串复制,当然,你得自己保证生命周期不出岔子.split 也返回 string_view,不然就等于把原来的一行数据又给复制至少一遍.
scenix
2017-10-26 11:39:19 +08:00
@wevsty 感谢你的回复,我重新检查了一下 cmake,虽然我开了 O3 优化,但是不小心打开了 gdb 的选项,去掉后速度明显上来了。
我编译了你的代码和我的代码,速度上有些不一样。能否在你的机器上编译运行一下我优化过的代码?看看速度会不会快些。
我没有用 string_view,stringstream,只是优化了插入部分的写法。
盼复。



#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);
const string key = line.substr(start, end - start);
// 寻找是否存在 key
auto iter = big_dict.find(key);
unordered_set<string> *p = NULL;
if(iter == big_dict.end()) {
p = new unordered_set<string>;
// 原来的写法是 big_dict[key]=p,需要查找一次,优化之
big_dict.insert(make_pair(key,p));
} else {
// 如果已经有结果了,直接暂存结果到指针 p 中
p = iter->second;
}

start = end+1;
while(start<line.size()) {
end = line.find_first_of(' ',start);
if(string::npos == end) end = line.size() - 1;
// 原来的写法是 big_dict[key]->insert,需要查找一次,优化之
p->insert(line.substr(start, end - start));
start = end + 1;
}
}
f_in.close();

}
int main() {

process_file(IN_FNAME, big_dict);

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

return 0;
}
scenix
2017-10-26 11:40:03 +08:00
@wevsty 少帖了几个 include 补上

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

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

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

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

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

© 2021 V2EX