【问题标题】:I think STL is causing my application triple its memory usage我认为 STL 导致我的应用程序的内存使用量增加了三倍
【发布时间】:2010-09-26 07:32:27
【问题描述】:

我在我的应用程序中输入了一个 200mb 的文件,由于一个非常奇怪的原因,我的应用程序的内存使用量超过了 600mb。我尝试过vector和deque,以及std::string和char *,但都无济于事。我需要我的应用程序的内存使用与我正在阅读的文件几乎相同,任何建议都会非常有帮助。 是否存在导致如此多内存消耗的错误?你能指出问题还是我应该重写整个事情?

Windows Vista SP1 x64、Microsoft Visual Studio 2008 SP1、32 位发行版、Intel CPU

到目前为止的整个应用程序:

#include <string>
#include <vector>
#include <iostream>
#include <iomanip>
#include <fstream>
#include <sstream>
#include <iterator>
#include <algorithm>
#include <time.h>



static unsigned int getFileSize (const char *filename)
{
    std::ifstream fs;
    fs.open (filename, std::ios::binary);
    fs.seekg(0, std::ios::beg);
    const std::ios::pos_type start_pos = fs.tellg();
    fs.seekg(0, std::ios::end);
    const std::ios::pos_type end_pos = fs.tellg();
    const unsigned int ret_filesize (static_cast<unsigned int>(end_pos - start_pos));
    fs.close();
    return ret_filesize;
}
void str2Vec (std::string &str, std::vector<std::string> &vec)
{
    int newlineLastIndex(0);
    for (int loopVar01 = str.size(); loopVar01 > 0; loopVar01--)
    {
        if (str[loopVar01]=='\n')
        {
            newlineLastIndex = loopVar01;
            break;
        }
    }
    int remainder(str.size()-newlineLastIndex);

    std::vector<int> indexVec;
    indexVec.push_back(0);
    for (unsigned int lpVar02 = 0; lpVar02 < (str.size()-remainder); lpVar02++)
    {
        if (str[lpVar02] == '\n')
        {
            indexVec.push_back(lpVar02);
        }
    }
    int memSize(0);
    for (int lpVar03 = 0; lpVar03 < (indexVec.size()-1); lpVar03++)
    {
        memSize = indexVec[(lpVar03+1)] - indexVec[lpVar03];
        std::string tempStr (memSize,'0');
        memcpy(&tempStr[0],&str[indexVec[lpVar03]],memSize);
        vec.push_back(tempStr);
    }
}
void readFile(const std::string &fileName, std::vector<std::string> &vec)
{
    static unsigned int fileSize = getFileSize(fileName.c_str());
    static std::ifstream fileStream;
    fileStream.open (fileName.c_str(),std::ios::binary);
    fileStream.clear();
    fileStream.seekg (0, std::ios::beg);
    const int chunks(1000); 
    int singleChunk(fileSize/chunks);
    int remainder = fileSize - (singleChunk * chunks);
    std::string fileStr (singleChunk, '0');
    int fileIndex(0);
    for (int lpVar01 = 0; lpVar01 < chunks; lpVar01++)
    {
        fileStream.read(&fileStr[0], singleChunk);
        str2Vec(fileStr, vec);
    }
    std::string remainderStr(remainder, '0');
    fileStream.read(&remainderStr[0], remainder);
    str2Vec(fileStr, vec);      
}
int main (int argc, char *argv[])
{   
        std::vector<std::string> vec;
        std::string inFile(argv[1]);
        readFile(inFile, vec);
}

【问题讨论】:

  • 您使用的是哪个 STL?在哪台机器上?
  • 一个非常非常小的文件的内存使用量是多少?
  • 您知道,您不需要需要将调用打开与 fstreams 分开,您可以这样做:std::ifstream file("whatever", std:: ios::二进制);此外,当 ifstream 对象被破坏时,它也会自动关闭。所以通常你也不需要显式关闭。
  • 您在 main 中的“inFile”变量也是完全没有意义的,因为 std::string 的构造函数采用 const char * 不是显式的。这意味着将 const char * 传递给采用 std::string 的函数将自动工作。
  • 还有! “memcpy(&tempStr[0],&str[indexVec[lpVar03]],memSize);”对我来说看起来很调皮,我不是标准律师,但我不确定 std::string 是否保证在内部是连续的(只有 c_str/data 返回一个连续的缓冲区。

标签: c++ memory memory-leaks stl


【解决方案1】:

您应该知道,因为您将fileStream 声明为static,所以它永远不会超出范围,这意味着文件直到执行的最后一刻才关闭。这肯定会涉及到一些记忆。您可以在最后一个 str2Vec 之前明确关闭它以尝试帮助解决这种情况。

此外,您多次打开和关闭同一个文件,只需打开一次并通过引用传递它(如果需要,重置状态)。虽然我想你可以通过文件的单次传递来实现你需要的东西。

哎呀,我怀疑你真的需要像你在这里做的那样知道文件大小,你可以只阅读大小“块”的数量,直到你得到一个简短的阅读(此时你完成了)。

你为什么不解释代码的目标,我觉得有一个非常更简单的解决方案可能。

【讨论】:

    【解决方案2】:

    STL 容器的存在是为了抽象出内存操作。如果你有一个硬内存限制,那么你就不能真正将它们抽象掉。

    我建议使用 mmap() 来读取文件(或者,在 Windows 中,MapViewOfFile())。

    【讨论】:

      【解决方案3】:

      我发现做行的最好方法是只读内存映射文件。不要为 \n 写 \0,而是使用成对的 const char *s,如 std::pair&lt;const char*, const char*&gt; 或成对的 const char*s 和计数。如果您需要编辑这些行,这是一个好方法是制作一个可以存储指针对或带有修改行的std::string的对象。

      至于使用 STL 向量或双端队列节省内存空间,一个好的技术是让它加倍,直到你完成添加。然后将其调整为实际大小,这应该将未使用的内存释放回堆分配器。内存可能仍会分配给程序,尽管我不担心。此外,不要采用默认大小,而是先获取文件大小(以字节为单位),然后除以您对每行平均字符的最佳猜测,并在开始时保留那么多空间。

      【讨论】:

        【解决方案4】:
        1. 不要使用 std::list。它需要比向量更多的内存。
        2. vector 执行所谓的“加倍”,即当空间不足时,它会分配 两倍 它当前拥有的内存。为了避免它,你可以使用 std::vector::reserve() 方法,如果我没记错的话,你可以使用 std::vector::capacity() 方法(注意容量() >= size())。

        由于在执行过程中不知道行数,我看不到简单的算法可以避免“加倍”问题。根据 slavy13.myopenid.com 的评论,解决方案是在完成阅读后将信息移动到另一个预先保留的向量(相关问题是How to downsize std::vector?)。

        【讨论】:

        • 我们事先不知道行数,所以使用reserve()是行不通的,使用reserve仍然有可能vector仍然会重新分配
        • 您可以在完成阅读后通过将其与临时副本交换来缩小它的大小。 stackoverflow.com/questions/253157/how-to-downsize-stdvector
        • 向量执行的双倍分配不会使字符串使用的内存量翻倍 - 只是存储在向量中的数组的大小 - 字符串使用的内存存储在堆上
        【解决方案5】:

        也许您应该详细说明为什么需要读取内存中的整个文件,我怀疑可能有一种方法可以做您想做的事情,而无需一次将整个文件读入内存。如果您真的需要此功能,请查看内存映射文件,这可能比您编写等效文件更有效率。然后,您的内部数据结构可以在文件中使用偏移量。顺便说一句,一定要看看你是否需要处理字符编码。

        【讨论】:

          【解决方案6】:

          我认为您尝试编写自己的缓冲策略是错误的。

          流已经实施了非常好的缓冲策略。如果您认为需要更大的缓冲区,您可以将基本缓冲区安装到流中,而无需任何额外的代码来控制缓冲区。

          这是我想出的: NB 用我在网上找到的“King James Bible”的文本版本进行了测试。

          #include <string>
          #include <vector>
          #include <list>
          #include <fstream>
          #include <algorithm>
          #include <iterator>
          #include <iostream>
          
          class Line: public std::string
          {
          };
          
          std::istream& operator>>(std::istream& in,Line& line)
          {
              // Relatively efficient way to copy a line into a string.
              return std::getline(in,line);
          }
          std::ostream& operator<<(std::ostream& out,Line const& line)
          {
              return out << static_cast<std::string const&>(line) << "\n";
          }
          
          void readLinesFromStream(std::istream& stream,std::vector<Line>& lines)
          {
              /*
               * Read into a list as this is flexible in memory usage and will not
               * allocate huge chunks of un-required space.
               *
               * Even with huge files the space for list will be insignificant
               * compared to the size of the data.
               *
               * This then allows us to reserve the correct size of the vector
               * Thus avoiding huge memory chunks being prematurely allocated that
               * are not required. It also prevents the internal structure from
               * being copied every time the container is re-sized.
               */
              std::list<Line>     data;
              std::copy(  std::istream_iterator<Line>(stream),
                          std::istream_iterator<Line>(),
                          std::inserter(data,data.end())
                       );
          
              /*
               * Reserve the correct size in the vector.
               * then copy out of the list into the vector
               */
              lines.reserve(data.size());
              std::copy(  data.begin(),
                          data.end(),
                          std::back_inserter(lines)
                       );
          }
          
          void readLinesFromFile(std::string const& name,std::vector<Line>& lines)
          {
              /*
               * Set up the file stream and override the default buffer used by the stream.
               * Make it big because we think the istream buffer is insufficient!!!!
               */
              std::ifstream       file;
              std::vector<char>   buffer(10000);
              file.rdbuf()->pubsetbuf(&buffer[0],buffer.size());
          
              file.open(name.c_str());
              readLinesFromStream(file,lines);
          }
          
          
          int main(int argc,char* argv[])
          {
              std::vector<Line>   lines;
              readLinesFromFile(argv[1],lines);
          
              // Un-comment if your file is larger than 1100 lines.
          
              // I tested with a copy of the King James bible. 
              // std::cout << "Lines: " << lines.size() << "\n";
              // std::copy(lines.begin() + 1000,lines.begin() + 1100,std::ostream_iterator<Line>(std::cout));
          }
          

          【讨论】:

            【解决方案7】:

            您可以做的另一件事是将整个文件加载到一个内存块中。然后制作一个指向每行第一个字符的指针向量,同时用 \0 替换换行符,使其以空值结尾。 (当然假设你的字符串不应该有 \0 。)

            它不一定像拥有一个字符串向量那样方便,但拥有一个 const char* 向量可能“一样好”。

            【讨论】:

              【解决方案8】:

              首先,您如何确定内存使用量?任务管理器不是一个合适的工具,因为它显示的实际上并不是内存使用情况。

              其次,除了您的(出于某种原因?)静态变量之外,当您完成读取文件时,唯一没有被释放的数据是向量。所以测试它的容量,并测试它包含的每个字符串的容量。找出他们每个人使用了多少内存。您拥有确定内存使用位置的工具。

              【讨论】:

                【解决方案9】:

                如果我没看错的话,最大的问题是该算法会自动将所需内存翻倍。

                在 ReadFile() 中,您将整个文件读入一组“singleChunk”大小的字符串(块),然后在 str2Vec() 的最后一个循环中,您为块的每个换行符分隔段分配一个临时字符串。所以你在那里将内存加倍。

                您还遇到了速度问题 - str2vec 会遍历块 2 次以查找所有换行符。没有理由你不能做到这一点。

                【讨论】:

                • 有很多代码可以用更惯用的 C++ 替换,更恰当地使用 STL。
                【解决方案10】:

                通过 pushBack() 增长向量会导致内存碎片和内存使用效率低下。我会尝试使用列表,并且仅在您确切知道它需要多少元素时才创建一个向量(如果您需要的话)。

                【讨论】:

                  【解决方案11】:

                  你的记忆正在支离破碎。

                  试试这样的:

                    HANDLE heaps[1025];
                    DWORD nheaps = GetProcessHeaps((sizeof(heaps) / sizeof(HANDLE)) - 1, heaps);
                  
                    for (DWORD i = 0; i < nheaps; ++i) 
                    {
                      ULONG  HeapFragValue = 2;
                      HeapSetInformation(heaps[i],
                                         HeapCompatibilityInformation,
                                         &HeapFragValue,
                                         sizeof(HeapFragValue));
                    }
                  

                  【讨论】:

                    【解决方案12】:

                    在 readFile 中,您至少有 2 个文件副本 - ifstream 和复制到 std::vector 中的数据。只要您打开文件,并且照原样复制它,就很难将总内存占用降至文件大小的两倍以下。

                    【讨论】:

                    • 但是 ifstream 不应该将整个文件内容保存在内存中。它只是一个缓冲区。
                    • @Roddy:当然,它是一个缓冲区——但大小是多少?有什么限制?由于 iostream 实际上是对底层操作系统功能的抽象,因此您必须查看较低级别的实现以查看当被要求打开文件时 它们 会做什么。我敢打赌,他们中的大多数都将其全部加载到内存中。
                    • @Harper !!!???嗯?您真的认为大多数操作系统文件打开都会将所有文件加载到内存中吗?真的吗?你真的这么认为吗?为什么?
                    • @Will Dean:只是基于我有一段时间没做过的东西的头顶概念——我已经被这样的代码困住了,以至于我正在对打开的记忆进行操作带有 iostream 的大文件,并让它占用内存。
                    【解决方案13】:

                    我不知道这是否相关,因为我真的不知道您的文件是什么样的。

                    但是您应该知道,在存储非常短的字符串时,std::string 可能会产生相当大的空间开销。而且,如果您为非常短的字符串单独更新 char*,您还将看到所有分配块开销。

                    您在该向量中放入了多少个字符串,它们的平均长度是多少?

                    【讨论】:

                      【解决方案14】:

                      尝试使用列表而不是向量。向量在内存中(几乎总是)是线性的。

                      当然,你有字符串,这些字符串(几乎总是)在修改时复制,引用计数应该减少这个问题,但它可能会有所帮助。

                      【讨论】:

                      • 一个列表可能只是使用更多的内存。它必须为每个列表元素存储两个额外的指针。由于缓存未命中,它的迭代速度也会慢很多。
                      • 完全同意,如果 .reserve() 与向量一起使用,列表肯定会使用更多内存。
                      • 我实际上并不确定,但我相信对于大多数 C++ 实现来说,std::string 类是not使用 ref 计数的写时复制方法实现的.
                      • 对 - 他的问题是他不知道有多少换行,所以他不能做保留。由于他不能做保留,他可能会在进行大约一百万次 push_back 时将内存碎片化(假设每行 200 个字符。)
                      猜你喜欢
                      • 1970-01-01
                      • 1970-01-01
                      • 2015-12-25
                      • 1970-01-01
                      • 2010-10-08
                      • 1970-01-01
                      • 1970-01-01
                      • 2013-09-22
                      • 1970-01-01
                      相关资源
                      最近更新 更多