在向量中存储重复字符串时节省内存?

Posted

技术标签:

【中文标题】在向量中存储重复字符串时节省内存?【英文标题】:Save memory when storing duplicate strings in a vector? 【发布时间】:2009-05-01 11:53:59 【问题描述】:

我使用的是 C++,它是 STL。 我有一个大 (100MB+) 文本文件。这个文件只有很多“单词”(由空格分隔的字符串),例如:

sdfi sidf ifids sidf assd fdfd fdfd ddd ddd

我需要将这些“单词”中的每一个都放在一个向量中:

vector<string> allWordsInFile;

所以对于我从文件中读取的每个单词,我都会这样做:

allWordsInFile.push_back(word);

该文件有很多重复的单词,我正在寻找节省内存的方法。每个单词都需要在向量中的正确位置表示。如果我可以只列出向量之外的所有单词,然后在向量中放置一个引用,那就太好了,但据我所知,不可能将引用放入向量中。然后我考虑只存储指向单词的指针,但是每个单词的长度太短了,我认为它不会有太大的区别? (在我的系统上,每个指针是 4 个字节,大多数字符串的大小可能差不多)。

有人可以建议另一种方法来解决这个问题吗?

【问题讨论】:

您希望文件中有多少个唯一词? 100MB 是当今大多数现成 PC 中可用 RAM 的 5% 到 10% - 这真的是个问题吗?为什么需要节省内存? "只是在向量中放一个引用,但这是不可能的[...]然后我想只存储指向单词的指针,但是每个单词的长度太短以至于我认为这不会有太大的不同”而且引用会更小,如何?在代码要求将它们存储在某个地方之前,它们只是零存储。我们可以通过创建一个唯一成员是 ref 的类,然后将其放入容器中来将 refs 放入容器中。并且正如大小和反汇编将显示的那样,所有现存的编译器都将引用实现为......指针,确切地说。很难想象另一种方式来做到这一点 【参考方案1】:

boost::flyweight 在这里看起来很有用。

事实上,tutorial example 显示 boost::flyweight&lt;std::string&gt; 被用于压缩数据库中的重复名称。

【讨论】:

我添加了一个单独的更详细的答案,它比较了字符串容器、flyweights 和将四个字符与 char* 联合的极端选项的效率。【参考方案2】:

如果您没有很多单词,您可以将单词存储在外部数组中,并将相应的索引存储在您的单词向量中。根据有多少唯一字,每个字只能有 1 个(最多 256 个字)或 2 个(最多 65536 个字)字节。

如果您需要速度,可以使用 std::map 在 log(n) 时间内查找字符串的索引(而不是遍历外部数组)

例如最大。 65536个独特词

vector<short> words
map<string,short> index
vector<string> uniqueWords
cindex = 0
for all words
    read the word
    if index[word] does not exist
        index[word] = cindex++
        uniqueWords.push_back(word)
    words.push_back(index[word]);

要检索原始单词,只需在 uniqueWords 中查找即可。

【讨论】:

目标是使用更少的内存。而且由于每个索引都存储一个 short 或 int 如果您需要超过 65536 个条目,我认为这个解决方案将使用更多 RAM。因为索引表的大小与整个单词列表的大小一样大。另外,您必须在此之上存储实际的唯一单词。如果发帖人的文件中有很长的字符串,那就太好了。但是因为他平均有4个字节的字符串,我认为这会消耗更多的内存。 @BrianR.Bondy 另一方面,std::string 除了指针和实际字符串值之外,还存储字符串的大小和当前保留的附加存储空间。因此,使用 short 而不是 std::string 可能会将文档中每个单词的开销从大约 16 个字节(或在 64 位系统上大约 32 个字节)减少到只有 2 个。 我没有对这样的东西进行基准测试,但是unordered_map 不能更好,因为不需要像map 那样对每个插入的内容进行排序?此外,由于[unordered_]map 已经处理确保唯一性并允许通过it-&gt;first 检索唯一键,OP 可能已经能够重用它作为最终结果,而不是维护基本的并行vector;y 完全多余的。【参考方案3】:

一种方法是存储一个仅包含唯一字符串的向量。然后,“单词”列表只是一个整数向量,作为唯一字符串数组的索引。这将以较慢的文件读取速度为代价来节省内存,因为您必须在 uniques 数组中为每个新单词进行某种线性扫描。然后,您可以使用映射作为唯一字符串数组的索引 - 如果在集合中找不到新单词,那么您知道将单词添加到唯一字符串列表的末尾。嘿,想想看,你甚至不需要矢量,因为地图就是为了这个目的:

typedef map<string, int> UniqueIndex;
UniqueIndex uniqueIndex;

typedef vector<int> WordsInFile;
WordsInFile wordsInFile;

for (each word in the file)

  UniqueIndex::const_iterator it=uniqueIndex.find(word);
  int index; // where in the "uniqueIndex" we can find this word
  if (it==uniqueIndex.end())
  
    // not found yet
    index=uniqueIndex.size();
    uniqueIndex[word]=index;
  
  else
    index=it.second;
  wordsInFile.push_back(index);

【讨论】:

如果你需要存储每个4字节的索引,它如何节省内存。而且他的每个字符串都大约是 4 个字节?所以你需要保留 4 个字节 * 字数 + 唯一字符串的大小。 好吧,如果唯一字符串的数量很少,您可以使用 16 位整数...不过我认为您的解决方案比我的要好 - 我投票支持您 我没有对类似的东西进行基准测试,但unordered_map 不能更好,因为不需要像map 那样对每个插入的内容进行排序?但无论如何,+1 指出生成的[unordered_] 映射可能会使vector 冗余,因为它们最终完全复制数据,并且用于呈现实际代码而不是其他类似答案的伪代码。 ;)【参考方案4】:

嗯,你真正想做的是压缩。

霍夫曼编码在这里可能会做得很好。您进行一次扫描以构建单词的频率表,然后应用 Huffman 算法为每个单词附加一个符号。 然后你组成一行位,代表具有适当顺序的单词。这行位可以被认为是你的“低内存向量”。

霍夫曼编码的本质让你可以在任何你想要的位置访问符号(没有符号是另一个符号的前缀),这里的问题是访问时间将是 O(n) 有一些优化可以减少访问时间,但只有一个常数因子,没有什么可以阻止它成为 O(n) 并且仍然保留少量内存使用。 如果您想了解可以完成的优化,请给我留言。

缺点:

    在构建编码字后,在 O(n) 中的访问,您必须线性扫描符号,直到到达适当的位置。 这个想法的实现绝非易事,而且会耗费您大量的时间。

编辑: 写这篇文章时我没有想到的一件事,你必须拿着查找表。因此,这可能只有在您有 很多 重复 字词时才有效。

霍夫曼编码: http://en.wikipedia.org/wiki/Huffman_coding

【讨论】:

“所以这可能只在你有很多重复的单词时才有效。”如果他不这样做,他就迷路了。没有办法压缩随机数据。 我只是提出一个想法,我没有说他必须实施,他会决定它是否适合他的数据。 我无意批评你的想法。我想你是对的。如果词是随机的,他迷路不是因为你的想法不好,而是因为他在原则上迷路了,任何人都无能为力。不过,更有可能的是,他的数据不是随机的。【参考方案5】:

由于您的字符串通常大约为 4 个字节,因此简单地创建另一个级别的间接将无济于事,因为指针的大小是 4 个字节(在 x86 上,或更糟糕的 x64 上的 8 个字节)。并且基于 int 的索引的大小也是 4 个字节。

分部加载:

您可以考虑分部分加载文件以节省内存。仅根据他们想要找到的单词位置加载您需要的内容。

您可以扫描文件一次以建立索引。该索引将存储每 10 个单词的起始位置(10 个任意选择)。

然后,如果您想访问单词 11,您将计算 11 除以 10 以获得该组的起始位置在索引中的位置,然后查找找到的起始位置。然后计算 11 模 10 以找出从该索引中读取多少个单词才能获得所需的单词。

此方法不会尝试消除存储重复字符串,但它将您需要使用的 RAM 限制为仅索引的大小。您可以将上面的“每 10 个单词”调整为“每 X 个单词”,以减少内存消耗。因此,您在 RAM 中使用的大小将仅为 (num_words_in_file/X)*sizeof(int) ,这比在 RAM 中存储整个文件的大小要小得多,即使您只存储了每个唯一字符串一次。

无多余空格访问每个单词:

如果您使用特定字符填充每个单词,以使每个单词大小相同,则在读入时忽略填充字符。您可以访问确切的单词,而无需额外的通过阶段来构建索引,并且几乎没有多余的空间。

【讨论】:

【参考方案6】:

你需要指定你需要在向量上快速进行哪些操作,否则无法设计出合适的解决方案。您需要主要是随机访问,还是主要是顺序访问?随机访问可以接受什么样的性能?

为了说明我的观点:存储数据的一种方法是使用 LZMA 或其他好的压缩库简单地压缩它们。然后当你需要访问某个元素时,你解压,一旦解压不再需要它们就丢弃解压数据。这样的存储将非常节省空间,顺序访问将相当快,但随机访问时间会非常糟糕。

【讨论】:

【参考方案7】:

如果您有可能不使用向量 - 另一种可能性,类似于上面的一些解决方案,但只有一个结构而不是两个结构,将单词映射到整数列表,每个整数代表位置,以及每次阅读单词时递增的计数变量:

   int count = 0;
   Map<String, List<Integer>> words = new HashMap<String, List<Integer>>();

然后它会变成类似(Java 类型的伪代码):

   for (word : yourFile) 
      List<Integer> positions = words.get(word);
      if (values == null) 
         positions = new ArrayList<Integer>();
      
      positions.add(++count);
      words.put(word, positions);
   

【讨论】:

std::map<:string std::vector> > 单词;整数位置 = 0; for(文件中的每个单词) words[word].push_back(position); ++位置; 【参考方案8】:

在另一个答案中将您指向boost::flyweight后, 我想仔细看看相对效率 由字符串、flyweights 和“核选项”组成的容器 用指针联合四个字符(类“sillystring” 在下面的代码中)。

代码注释:

使用 g++ 4.3.3 和 boost 1.38.0 在 32 位 Debian/squeeze 上工作 使用std::deque 而不是std::vector,因为向量的 大小加倍行为(c.f deques' chunks)给出了一个 (可以说)低效率的误导性印象,如果 测试用例最近刚好翻了一番。 “sillystring”使用指针的 LSB 指向 将指针用例与本地字符区分开来 案子。这应该有效,除非您的 malloc 分配 在奇数字节边界上(在这种情况下代码将抛出) (当然在我的系统上没有看到这个;YMMV)。 在任何人觉得有必要指出之前,是的,我愿意 考虑一下这个可怕的危险hacky代码,而不是 选项可以轻而易举地选择。

结果因字长分布而异, 但是对于分布“(2D6 + 1)/ 2”(因此峰值为4,但具有 长度从 1 到 6),效率(定义为比率 实际内存消耗与实际数量之间 需要存储的字符)是:

12.4% deque&lt;string&gt; 20.9% deque&lt;flyweight&lt;string&gt; &gt; 43.7% deque&lt;sillystring&gt;

如果所有您的单词都是 4 个字符(在单词生成器中更改为 const int length=4;),这是 sillystring 的理想情况,那么您会得到:

14.2% deque&lt;string&gt; 77.8% deque&lt;flyweight&lt;string&gt; &gt; 97.0% deque&lt;sillystring&gt;

所以享元当然是一个快速的改进,但是你可以通过利用你的单词适应指针大小的空间并避免额外的堆开销来做得更好。

代码如下:

// Compile with "g++ -O3 -o fly fly.cpp -lpthread"
// run "./fly 0 && ./fly 1 && ./fly 2" 

#include <boost/flyweight.hpp>
#include <boost/format.hpp>
#include <boost/random.hpp>
#include <cstring>
#include <deque>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>

#include <sys/types.h>
#include <unistd.h>

#define THROW(X,MSG) throw X(boost::str(boost::format("%1%: %2%") % __PRETTY_FUNCTION__ % MSG))

struct random_word_generator

  random_word_generator(uint seed)
    :_rng(seed),
     _length_dist(1,6),
     _letter_dist('a','z'),
     _random_length(_rng,_length_dist),
     _random_letter(_rng,_letter_dist)
  
  std::string operator()()
  
    std::string r;
    const int length=(_random_length()+_random_length()+1)/2;
    for (int i=0;i<length;i++) r+=static_cast<char>(_random_letter());
    return r;
  
private:
  boost::mt19937 _rng;
  boost::uniform_int<> _length_dist;
  boost::uniform_int<> _letter_dist;
  boost::variate_generator<boost::mt19937&,boost::uniform_int<> > 
    _random_length;
  boost::variate_generator<boost::mt19937&,boost::uniform_int<> > 
    _random_letter;
;

struct collector

  collector()
  virtual ~collector()

  virtual void insert(const std::string&)
    =0;
  virtual void dump(const std::string&) const
    =0;
;

struct string_collector
  : public std::deque<std::string>, 
    public collector

  void insert(const std::string& s)
  
    push_back(s);
  
  void dump(const std::string& f) const
  
    std::ofstream out(f.c_str(),std::ios::out);
    for (std::deque<std::string>::const_iterator it=begin();it!=end();it++)
      out << *it << std::endl;
  
;

struct flyweight_collector 
  : public std::deque<boost::flyweight<std::string> >, 
    public collector

  void insert(const std::string& s)
  
    push_back(boost::flyweight<std::string>(s));
  
  void dump(const std::string& f) const
  
    std::ofstream out(f.c_str(),std::ios::out);
    for (std::deque<boost::flyweight<std::string> >::const_iterator it=begin();
         it!=end();
         it++
         )
      out << *it << std::endl;
  
;

struct sillystring

  sillystring()
  
    _rep.bits=0;
  
  sillystring(const std::string& s)
  
    _rep.bits=0;
    assign(s);
  
  sillystring(const sillystring& s)
  
    _rep.bits=0;
    assign(s.str());
  
  ~sillystring()
  
    if (is_ptr()) delete [] ptr();
  
  sillystring& operator=(const sillystring& s)
  
    assign(s.str());
  
  void assign(const std::string& s)
  
    if (is_ptr()) delete [] ptr();
    if (s.size()>4)
      
        char*const p=new char[s.size()+1];
        if (reinterpret_cast<unsigned int>(p)&0x00000001)
          THROW(std::logic_error,"unexpected odd-byte address returned from new");
        _rep.ptr.value=(reinterpret_cast<unsigned int>(p)>>1);
        _rep.ptr.is_ptr=1;
        strcpy(ptr(),s.c_str());
      
    else
      
        _rep.txt.is_ptr=0;
        _rep.txt.c0=(s.size()>0 ? validate(s[0]) : 0);
        _rep.txt.c1=(s.size()>1 ? validate(s[1]) : 0);
        _rep.txt.c2=(s.size()>2 ? validate(s[2]) : 0);
        _rep.txt.c3=(s.size()>3 ? validate(s[3]) : 0);
      
  
  std::string str() const
  
    if (is_ptr())
      
        return std::string(ptr());
      
    else
      
        std::string r;
        if (_rep.txt.c0) r+=_rep.txt.c0;
        if (_rep.txt.c1) r+=_rep.txt.c1;
        if (_rep.txt.c2) r+=_rep.txt.c2;
        if (_rep.txt.c3) r+=_rep.txt.c3;
        return r;
      
  
private:
  bool is_ptr() const
  
    return _rep.ptr.is_ptr;
  
  char* ptr()
  
    if (!is_ptr())
      THROW(std::logic_error,"unexpected attempt to use pointer");
    return reinterpret_cast<char*>(_rep.ptr.value<<1);
  
  const char* ptr() const
  
    if (!is_ptr()) 
      THROW(std::logic_error,"unexpected attempt to use pointer");
    return reinterpret_cast<const char*>(_rep.ptr.value<<1);
  
  static char validate(char c)
  
    if (c&0x80)
      THROW(std::range_error,"can only deal with 7-bit characters");
    return c;
  
  union
  
    struct
    
      unsigned int is_ptr:1;
      unsigned int value:31;
     ptr;
    struct
    
      unsigned int is_ptr:1;
      unsigned int c0:7;
      unsigned int :1;
      unsigned int c1:7;
      unsigned int :1;
      unsigned int c2:7;      
      unsigned int :1;
      unsigned int c3:7;      
     txt;
    unsigned int bits;
   _rep;
;

struct sillystring_collector 
  : public std::deque<sillystring>, 
    public collector

  void insert(const std::string& s)
  
    push_back(sillystring(s));
  
  void dump(const std::string& f) const
  
    std::ofstream out(f.c_str(),std::ios::out);
    for (std::deque<sillystring>::const_iterator it=begin();
         it!=end();
         it++
         )
      out << it->str() << std::endl;
  
;

// getrusage is useless for this; Debian doesn't fill out memory related fields
// /proc/<PID>/statm is obscure/undocumented
size_t memsize()

  const pid_t pid=getpid();
  std::ostringstream cmd;
  cmd << "awk '($1==\"VmData:\")print $2,$3;' /proc/" << pid << "/status";
  FILE*const f=popen(cmd.str().c_str(),"r");
  if (!f)
    THROW(std::runtime_error,"popen failed");
  int amount;
  char units[4];
  if (fscanf(f,"%d %3s",&amount,&units[0])!=2)
    THROW(std::runtime_error,"fscanf failed");
  if (pclose(f)!=0)
    THROW(std::runtime_error,"pclose failed");
  if (units[0]!='k' || units[1]!='B')
    THROW(std::runtime_error,"unexpected input");
  return static_cast<size_t>(amount)*static_cast<size_t>(1<<10);


int main(int argc,char** argv)

  if (sizeof(void*)!=4)
    THROW(std::logic_error,"64-bit not supported");
  if (sizeof(sillystring)!=4) 
    THROW(std::logic_error,"Compiler didn't produce expected result");

  if (argc!=2)
    THROW(std::runtime_error,"Expected single command-line argument");

  random_word_generator w(23);

  std::auto_ptr<collector> c;
  switch (argv[1][0])
    
    case '0':
      std::cout << "Testing container of strings\n";
      c=std::auto_ptr<collector>(new string_collector);
      break;
    case '1':
      std::cout << "Testing container of flyweights\n";
      c=std::auto_ptr<collector>(new flyweight_collector);
      break;
    case '2':
      std::cout << "Testing container of sillystrings\n";
      c=std::auto_ptr<collector>(new sillystring_collector);
      break;
    default:
      THROW(std::runtime_error,"Unexpected command-line argument");
    

  const size_t mem0=memsize();

  size_t textsize=0;
  size_t filelength=0;
  while (filelength<(100<<20))
    
      const std::string s=w();
      textsize+=s.size();
      filelength+=(s.size()+1);
      c->insert(s);
    

  const size_t mem1=memsize();
  const ptrdiff_t memused=mem1-mem0;

  std::cout 
    << "Memory increased by " << memused/static_cast<float>(1<<20)
    << " megabytes for " << textsize/static_cast<float>(1<<20)
    << " megabytes text; efficiency " << (100.0*textsize)/memused << "%"
    << std::endl;

  // Enable to verify all containers stored the same thing:
  //c->dump(std::string(argv[1])+".txt");

  return 0;

【讨论】:

【参考方案9】:

我认为使用这样的东西会节省内存:

struct WordInfo

    std::string m_word;
    std::vector<unsigned int> m_positions;
;

typedef std::vector<WordInfo> WordVector;

First find whether the word exists in WordVector

If no,
    create WordInfo object and push back into WordVector
else
    get the iterator for the existing WordInfo
    Update the m_positions with the position of the current string

【讨论】:

或者你可以只使用 std::pair。【参考方案10】:

首先,我们应该知道字符串是什么样的:

如果“大多数字符串是 4 个字母”并且文件是 100MB,那么

a) 必须有很多重复项,也许你最好将 not 的字符串存储在数组中(特别是如果你可以忽略这种情况),但这不会给你他们的在向量中的位置。

b) 也许有一种方法可以从 ascii 8 位(假设它确实是 ASCII)(8X4=32)压缩到 20 位(5x4),每个字母使用 5 位,并且使用一些花哨的位工作减小向量的大小。请运行数据样本并查看文件中确实有多少不同的字母,也许某些字母组非常丰富以至于为它们分配一个特殊值(在 8 位序列中的 32 个选项中)是有意义的。实际上,如果我是正确的,如果大多数单词都可以转换为 20 位表示,那么只需要一个 3MB 的数组来存储所有单词及其字数 - 并分别处理 >4 个字符(假设 3 个字节是字数应该足够了,也许 2 个字节就足够了:可以使它动态化,总共使用 2MB)

c) 另一个不错的选择是,我认为上面有人说过,只需将字符串与字母连接起来并在其上运行压缩器,坏事是 cpu 负载和它可能需要压缩的临时内存/ /解压缩。除此之外应该工作得很好

d) 如果你真的想尽量减少使用的内存,也许你想使用你的磁盘:对单词进行排序(如果没有足够的内存,你可以使用磁盘),然后一个接一个地创建一个带有单词的临时文件,这将适用于顺序访问。然后,您可以创建单词的一次性树状表示,其叶子包含文件中单词的相对地址以进行随机访问,并将其“序列化”到磁盘。最后,由于大多数单词的长度为 4 个字符,因此只需 5 个跃点,您就可以在不使用任何 ram 的情况下获得文件中任何字母的位置,可以这么说。您还可以将树的前 2 或 3 层缓存在 ram 中,这将是轻量级的,以将 4 个字符的单词的跳跃减少到 2 或 3 次。然后你可以使用一些小内存来缓存最常用的单词,并做各种细节来加快访问速度。

已经很晚了,希望我有道理......

pd。感谢 cmets 伙计们

【讨论】:

我也开始描述一个 trie,但后来删除它,因为他需要保留单词编号,并且为此您需要存储等量的内存(因为每个字符串约为 4 个字节) 除了尝试之外,有向无环词图可能更适合,但同样,它不保留顺序。

以上是关于在向量中存储重复字符串时节省内存?的主要内容,如果未能解决你的问题,请参考以下文章

在函数中使用向量指针时出错

预先分配的向量向量,但在填充它时仍然线性增加内存

用于在内存中存储字符串数组的数据结构

python的内存驻留机制(小数据池)

带有向量作为键的 STL 映射

如何合理的使用Redis内存