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

时间:2009-05-01 11:53:59

标签: c++ string memory vector

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

sdfi sidf ifids sidf assd fdfd fdfd ddd ddd

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

vector<string> allWordsInFile;

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

allWordsInFile.push_back(word);

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

有人可以提出另一种解决方法吗?

10 个答案:

答案 0 :(得分:7)

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

实际上tutorial example显示boost::flyweight<std::string>用于压缩数据库中名称的重复。

答案 1 :(得分:3)

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

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

e.g。最大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中查找。

答案 2 :(得分: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);
}

答案 3 :(得分:3)

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

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

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

缺点:

  1. 在您构建了编码的单词后,在O(n)中进行访问,您必须线性扫描符号,直到找到合适的位置。
  2. 实现这个想法并不是微不足道的,而且会花费你很多时间。
  3. 修改: 在编写帖子时我没有考虑过的一件事,你必须持有查找表。所以这可能只有在很多重复字词时才有效。

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

答案 4 :(得分:2)

由于你的字符串通常大约只有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中的大小要小得多,即使你只存储了每个唯一的字符串一次。

访问每个单词,没有多余空格

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

答案 5 :(得分:2)

您需要指定在矢量上需要快速操作的操作,否则无法设计合适的解决方案。您是否需要大多数随机访问,或者主要是顺序访问随机访问可接受哪些性能?

为了说明我的观点:存储数据的一种方法是使用LZMA或其他好的压缩库来压缩它们。然后,当您需要访问某个元素时,只要解压缩不再需要它们,就会解压缩,丢弃解压缩数据。这样的存储空间效率非常高,顺序访问速度合理,但随机访问时间会非常糟糕。

答案 6 :(得分:1)

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

   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);
   }

答案 7 :(得分:1)

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

关于代码的说明:

  • 使用g ++ 4.3.3和增强1.38.0
  • 在32位Debian / squeeze上工作
  • 使用std::deque代替std::vector因为矢量而已 大小倍增行为(c.f deques'unk)给出了一个 (可以说)如果说是低效率的误导性印象 最近,测试用例恰好翻了一番。
  • “sillystring”使用指针的LSB 区分指针用例和本地字符 案件。除非你的malloc分配,否则这应该有效 在奇数字节边界上(在这种情况下代码将抛出) (当然没有在我的系统上看到过这个; YMMV)。 在任何人觉得需要指出之前,是的,我做到了 考虑这个可怕的危险hacky代码而不是 选择可以轻松选择。

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

  • 12.4%deque<string>
  • 20.9%deque<flyweight<string> >
  • 43.7%deque<sillystring>

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

  • 14.2%deque<string>
  • 77.8%deque<flyweight<string> >
  • 97.0%deque<sillystring>

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

以下是代码:

// 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;
}

答案 8 :(得分:0)

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

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

答案 9 :(得分:0)

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

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

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

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

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

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

现在已经很晚了,希望我有意义......

概率pd。 thnx的评论家伙