如何在C++中减少数据的内存大小

How to reduce memory size for data in C++?

本文关键字:数据 内存 C++      更新时间:2023-10-16

我正在开发C++,并使用multimap存储数据。

 struct data
 {
      char* value1;
      char* value2;
      data(char* _value1, char* _value2)
      {
           int len1 = strlen(_value1);
           value1 = new char[len1+1];
           strcpy(value1,_value1);
           int len2 = strlen(_value2);
           value2 = new char[len2+2];
           strcpy(value2,_value2);
      }
      ~data()
      {
           delete[] value1;
           delete[] value2;
      }
 }
 struct ltstr
 {
     bool operator()(const char* s1, const char* s2) const
     {
          return strcmp(s1, s2) < 0;
     }
 };

 multimap <char*, data*, ltstr> m;

样本输入:

  Key               Value
  ABCD123456        Data_Mining Indent Test Fast Might Must Favor List Myself Janki Jyoti Sepal Petal Catel Katlina Katrina Tesing Must Motor blah blah.
  ABCD123456        Datfassaa_Minifasfngf Indesfsant Tfdasest Fast Might Must Favor List My\fsad\self Jfasfsa Katrifasdna Tesinfasfg Must Motor blah blah.
  tretD152456       fasdfa fasfsaDfasdfsafata_Mafsfining Infdsdent Tdfsest Fast Might Must Favor List Myself Janki

输入的条目有2700万个。输入大小=14GB

但我注意到内存消耗达到了56GB。我可以知道如何减少内存大小吗?

如果你不能减少实际存储的数据量,你可能想尝试使用一个开销较小的不同容器(map和multimap有相当多的开销),或者找到一种只在内存中保留部分数据的方法。

你可能想看看这些库:

  • STXXL:http://stxxl.sourceforge.net/

  • 谷歌CPP Btree:https://code.google.com/p/cpp-btree/

一种可能性是使用std::map<char *, std::vector<data> >而不是多映射。在多映射中,您将密钥字符串存储在每个条目中。对于映射,您只有一个密钥字符串的副本,并附加多个data项。

第一个优化是存储data对象,而不是指针

std::multimap <char*, data, ltstr> m;

因为使用CCD_ 4增加了用于分配的额外存储器开销。

另一种是使用池分配器/内存池来减少动态内存分配的占用空间。

如果您有许多相同的键串,如果您可以重用这些键,那么您也可以改进这一点。

我怀疑您正在泄漏或不必要地复制密钥中的内存。关键char *字符串来自哪里?如何管理它们的内存?

如果它们与数据对象中的字符串相同,请考虑使用multiset<data *, ltdata>而不是multimap

如果有许多重复的字符串,请考虑在set<char *,ltstr>中池化字符串以消除重复。

在没有看到一些数据的情况下,有几件事可以提高项目的内存使用率。

首先,正如Olaf所建议的,将数据对象存储在多映射中,而不是指向它的指针。不过,我不建议为数据结构使用池,与直接将其存储在映射中相比,这只会使事情变得复杂,而不节省内存。

不过,您可以为映射指定一个专门的分配器来分配std::pair<char*, data>对象。这可以节省一些开销和堆碎片。

接下来,您应该关注的主要事项是尝试去掉数据中的两个char*指针。有14个演出的数据,必须有一些重叠。根据它是什么数据,您可以以不同的方式存储它。

例如,如果数据是名称或关键字,那么将它们存储在中心散列中是有意义的。是的,有更复杂的解决方案,比如上面建议的DAWG,但我认为应该先尝试简单的解决方案。

只需将其存储在std::set<std::string>中并将迭代器存储到其中,就可以压缩所有重复项,从而节省大量数据。不过,这是假设您没有删除字符串。删除字符串将需要进行一些引用计数,因此您将使用类似std::map<std::string, unsinged long>的内容。我建议您编写一个继承自/包含此哈希的类,而不是将引用计数逻辑放入您的数据类中。

但是,如果您存储的数据没有太多重叠,例如,因为它是二进制数据,那么我建议您将其存储在std::stringstd::vector<char>中。原因是现在您可以去掉数据结构中的逻辑,甚至用std::pair替换它。

我还假设您的键不是存储在数据结构中的指针之一。如果是,那么一定要去掉它,并在多映射中使用std::pairfirst属性。

根据您存储的数据类型,可能会有进一步的改进。

因此,有了很多可能不适用于您的数据的假设,您可能只有以下几点:

typedef std::set<std:string> StringMap;
typedef StringMap::const_iterator StringRef;
typedef std::multimap<StringRef, std::pair<StringRef, StringRef>> DataMap;

我仍然不完全确定这里发生了什么,但内存开销似乎至少是问题的一部分。然而,总内存消耗大约是data结构所需的4倍。如果有27M条记录占用14GB,那么每条记录大约有500个字节,但占用的空间是56GB。对我来说,这表明存储的数据比我们这里显示的要多,或者至少有一些数据存储了多次。

"堆存储的额外数据"并没有真正为我做到这一点。在linux中,内存分配至少需要32字节左右的数据。16字节的开销,并且分配的存储器本身占用16字节的倍数。

因此,对于多映射中存储的一个data *记录,我们需要:

 16 bytes of header for the memory allocation
 8 bytes for pointer of `value1`
 8 bytes for pointer of `value2`
 16 bytes of header for the string in value1
 16 bytes of header for the string in value2
 8 bytes (on average) "size rounding" for string in value 1
 8 bytes (on average) "size rounding" for string in value 2
 ?? bytes from the file. (X)
 80 + X bytes total. 

然后我们在多映射中有char *

 16 bytes of header for the memory allocation. 
 8 bytes of rounding on average. 
 ?? bytes from the file. (Y)
 24 + Y bytes total. 

多映射的每个节点都有两个指针(我假设它是某种二进制树):

 16 bytes of header for the memory allocation of the node. 
 8 bytes of pointer to "left"
 8 bytes of pointer to "right"
 32 bytes total. 

因此,文件中每个条目的"开销"为136字节。对于2700万条记录,这一数字略高于4GB。

正如我所说,该文件每个条目包含500个字节,因此为14GB。

总共18GB。

所以,在某个地方,要么有东西泄漏,要么数学错误。我可能会偏离我的计算,但即使上面的所有内容都占用了我计算的两倍空间,仍然有20GB的空间没有计算出来。

当然,我们可以做一些事情来节省内存:

1) 不要在data中分配两个字符串。首先计算两个长度,分配一块内存,然后将字符串依次存储:

  data(char* _value1, char* _value2)
  {
       int len1 = strlen(_value1);
       int len2 = strlen(_value2);
       value1 = new char[len1 + len2 +2];
       strcpy(value1,_value1);
       value2 = value1 + len1 + 1; 
       strcpy(value2,_value2);
  }

这将为每个条目平均节省24个字节。我们可以通过巧妙地同时为数据value1和value2分配内存来节省更多。但这可能有点"太聪明了"。

2) 分配一大块data物品,并一次分发一个也会有所帮助。为了实现这一点,我们需要一个空的构造函数和一个"setvalues"方法:

struct data
{
    ...
    data() {};
    ... 
    set_values(char* _value1, char* _value2)
    {
         int len1 = strlen(_value1);
         int len2 = strlen(_value2);
         value1 = new char[len1 + len2 +2];
         strcpy(value1,_value1);
         value2 = value1 + len1 + 1; 
         strcpy(value2,_value2);
    }
}
std::string v1[100], v2[100], key[100];
for(i = 0; i < 100; i++)
{
    if (!read_line_from_file(key[i], v1[i], v2[i]))
    {
        break;
    }
}    
data* data_block = new data[i]; 
for(j = 0; j < i; j++)
{
    data_block[j].setValues[v1[j].c_str(), v2[j].c_str());
    m.insert(key[i].c_str(), &data_block[j]);
}

同样,这不会节省大量内存,但每个16字节的区域都会节省一些内存。以上当然不是完整的代码,更多的是"如何做到这一点的说明"。

3) 我仍然不确定"密钥"在多映射中来自哪里,但如果密钥是value1和value2条目之一,那么您可以重用其中一个条目,而不是存储另一个副本(假设目前是这样做的)。

如果这不是一个真实的答案,我很抱歉,但我相信这是一个答案,从某种意义上说,"在你对自己所做事情的解释中,有些事情是不负责任的"。

了解你的计划中进行了哪些分配肯定会有所帮助。