矢量矢量的意外巨大内存消耗

时间:2017-12-30 19:44:03

标签: c++ arrays vector

使用变长ASCII字处理dict文件。

constexpr int MAXLINE = 1024 * 1024 * 10; // total number of words, one word per line.

目标:将整个文件读入内存,并能够通过索引访问每个单词。

我希望通过索引快速访问每个单词。我们可以使用二维数组来实现它;但是需要设置MAXLENGTH,而不是提及MAXLENGTH未知。

constexpr int MAXLENGTH= 1024; // since I do not have the maximum length of the word
char* aray = new char[MAXLINE * MAXLENGTH];

如果大多数单词短于MAXLENGTH,则上述代码不会对内存有用。而且有些单词可能比MAXLENGTH长,导致错误。

对于变体长度对象,我认为vector可能最适合此问题。所以我想出了矢量矢量来存储它们。

vector<vector<char>> array(MAXLINE);

这看起来很有希望,直到我意识到情况并非如此。

我在带有MAXLINE 4个ASCII字符的dict文件上测试了这两种方法。(这里所有单词都是4个字符的单词)

constexpr int MAXLINE = 1024 * 1024 * 10;

如果我使用new运算符来存储数组,(这里MAXLENGTH只是4)

char* aray = new char[MAXLINE * 4]; 

内存消耗大约为40MB。但是,如果我尝试使用vector来存储(我将char更改为int32_t只适合四个字符)

vector<vector<int32_t>> array(MAXLINE);

你也可以使用char vector,并为4个字符保留空间。

vector<vector<char>> array(MAXLINE);
for (auto & c : array) {
    c.reserve(4);
}

内存消耗量上升到大约720MB(调试模式),280MB(发布模式),这是出乎意料的高,有人可以给我一些解释,以澄清原因。

obseravation:向量的大小取决于实现,如果您在调试模式下进行编译。

与我的系统一样

sizeof(vector<int32_t>) = 16  //  debug mode

sizeof(vector<int32_t>) = 12  // release mode

在调试模式下,720MB的内容消耗为vector<vector<int32_t>> array(MAXLINE);,而实际向量仅需sizeof(vector<int32_t>) * MAXLINE = 16 * 10MB = 160 MB

在相对模式中,内容消耗为280MB,但预期值为sizeof(vector<int32_t>) * MAXLINE = 12 * 10MB = 120 MB

有人可以解释实际内存消耗和预期消耗的巨大差异(根据子矢量大小计算)。

欣赏,新年快乐!

5 个答案:

答案 0 :(得分:3)

对于你的情况:

  

所以,这是否意味着矢量矢量不是一个好主意存储小   对象? -

一般没有。嵌套的sub-vector并不是存储大量可变大小序列的好方法。例如,您不想表示允许变量多边形(三角形,四边形,五边形,六边形,n形)的索引网格,每个多边形使用单独的std::vector个实例,否则您需要&#39 ; ll倾向于炸掉内存使用并且有一个非常缓慢的解决方案:因为每个单个怪异多边形都涉及堆分配,并且内存中存在爆炸性因为向量通常会为元素预先分配一些内存以及存储大小和如果你有大量的青少年序列,那么这些能力往往比你需要的还大。

vector是一个出色的数据结构,可以连续存储一百万个东西,但不太适合存储一百万个小矢量。

在这种情况下,即使是单链接的索引列表也可以更好地处理指向更大向量的索引,执行速度更快,有时甚至使用更少的内存,尽管32位链接开销,如下所示:

enter image description here

那说,对于你的特殊情况,有一个很大的&ol;随机访问可变长度字符串序列,这是我的建议:

// stores the starting index of each null-terminated string
std::vector<int> string_start;

// stores the characters for *all* the strings in one single vector.
std::vector<char> strings;

这会将开销减少到每个字符串条目接近32位(假设int是32位),并且您将不再需要为添加的每个字符串条目分配单独的堆。

读完所有内容后,可以通过压缩来最小化内存使用以截断阵列(消除任何多余的保留容量):

// Compact memory use using copy-and-swap.
vector<int>(string_start).swap(string_start);
vector<char>(strings).swap(strings);

现在要检索第n个字符串,您可以这样做:

const char* str = strings.data() + string_start[n];

如果您还需要搜索功能,您实际上可以存储一大堆字符串并快速搜索它们(包括基于前缀的搜索等内容),即使上述解决方案使用{{3 }}。它虽然是一个涉及的解决方案,但如果您的软件围绕字符串字典并搜索它们可能是值得的,您可能只能找到一些已经为其提供的第三方库。

<强>的std :: string

为了完整起见,我想我会提到std::string。最近的实现通常通过预先存储缓冲区来优化小字符串,该缓冲区不是单独堆分配的。但是,在您的情况下,可能会导致更多的爆炸性内存使用,因为这会使sizeof(string)更大的方式消耗比真正短字符串所需的内存更多的内存。它确实使std::string对于临时字符串更有用,这使得如果你按需提取{c}大字符的std::string就可以得到完全正常的东西:

std::string str = strings.data() + string_start[n];

......而不是:

const char* str = strings.data() + string_start[n];

也就是说,字符的大向量会为存储所有字符串提供更好的性能和内存。一般来说,如果你想储存数以百万计的小容器,任何类型的通用容器都会停止运行。

主要的概念问题是,当需要数百万个可变大小的序列时,需求的可变大小性质与容器的广义性质相结合将意味着你有一百万个很小的内存管理器,所有必须在堆上进行潜在分配,或者如果不是,则分配比所需数据更多的数据,并且如果它是连续的,则跟踪其大小/容量,等等。不可避免的是,他们自己记忆中的一百万+经理会变得相当昂贵。

因此,在这些情况下,放弃“完全,独立”的便利通常是值得的。容器,而是使用一个巨型缓冲区,或一个存储元素数据的巨型容器(如vector<char> strings的情况),以及另一个索引或指向它的大容器,如vector<int> string_start的情况。有了这个,你可以用两个大容器而不是一百万个小容器代表类似的一百万个可变长度字符串。

删除第n个字符串

你的情况听起来并不像你需要删除一个字符串条目,只需要加载和访问,但是如果你需要删除一个字符串,当所有的字符串和索引到它们的起始位置时会变得棘手存储在两个巨大的缓冲区中。

如果您想这样做,我建议您不要立即从缓冲区中删除字符串。相反,你可以这样做:

// Indicate that the nth string has been removed.
string_start[n] = -1;

在迭代可用字符串时,只需跳过string_start[n]-1的字符串。然后,在删除了许多字符串后,不时地使用压缩内存,请执行以下操作:

void compact_buffers(vector<char>& strings, vector<int>& string_start)
{
    // Create new buffers to hold the new data excluding removed strings.
    vector<char> new_strings;
    vector<int> new_string_start;
    new_strings.reserve(strings.size());
    new_string_start.reserve(string_start.size());

    // Store a write position into the 'new_strings' buffer.
    int write_pos = 0;

    // Copy strings to new buffers, skipping over removed ones.
    for (int start: string_start)
    {
        // If the string has not been removed:
        if (start != -1)
        {
            // Fetch the string from the old buffer.
            const char* str = strings.data() + start;

            // Fetch the size of the string including the null terminator.            
            const size_t len = strlen(str) + 1;

            // Insert the string to the new buffer.
            new_strings.insert(new_strings.end(), str, str + len);

            // Append the current write position to the starting positions
            // of the new strings.
            new_string_start.push_back(write_pos);

            // Increment the write position by the string size.
            write_pos += static_cast<int>(len);
        }
    }

    // Swap compacted new buffers with old ones.
    vector<char>(new_strings).swap(strings);
    vector<int>(new_string_start).swap(string_start);
}

您可以在删除多个字符串后定期调用上述内容以压缩内存使用。

字符串序列

这里有一些代码将所有这些东西放在一起,你可以随意使用和修改它们。

////////////////////////////////////////////////////////
// StringSequence.hpp:
////////////////////////////////////////////////////////
#ifndef STRING_SEQUENCE_HPP
#define STRING_SEQUENCE_HPP

#include <vector>

/// Stores a sequence of strings.
class StringSequence
{
public:
    /// Creates a new sequence of strings.
    StringSequence();

    /// Inserts a new string to the back of the sequence.
    void insert(const char str[]);

    /// Inserts a new string to the back of the sequence.
    void insert(size_t len, const char str[]);

    /// Removes the nth string.
    void erase(size_t n);

    /// @return The nth string.
    const char* operator[](size_t n) const;

    /// @return The range of indexable strings.
    size_t range() const;

    /// @return True if the nth index is occupied by a string.
    bool occupied(size_t n) const;

    /// Compacts the memory use of the sequence.
    void compact();

    /// Swaps the contents of this sequence with the other.
    void swap(StringSequence& other);

private:
    std::vector<char> buffer;
    std::vector<size_t> start;
    size_t write_pos;
    size_t num_removed;
};

#endif


////////////////////////////////////////////////////////
// StringSequence.cpp:
////////////////////////////////////////////////////////
#include "StringSequence.hpp"
#include <cassert>

StringSequence::StringSequence(): write_pos(1), num_removed(0)
{
    // Reserve the front of the buffer for empty strings.
    // We'll point removed strings here.
    buffer.push_back('\0');
}

void StringSequence::insert(const char str[])
{
    assert(str && "Trying to insert a null string!");
    insert(strlen(str), str);
}

void StringSequence::insert(size_t len, const char str[])
{
    const size_t str_size = len + 1;
    buffer.insert(buffer.end(), str, str + str_size);
    start.push_back(write_pos);
    write_pos += str_size;
}

void StringSequence::erase(size_t n)
{
    assert(occupied(n) && "The nth string has already been removed!");
    start[n] = 0;
    ++num_removed;
}

const char* StringSequence::operator[](size_t n) const
{
    return &buffer[0] + start[n];
}

size_t StringSequence::range() const
{
    return start.size();
}

bool StringSequence::occupied(size_t n) const
{
    return start[n] != 0;
}

void StringSequence::compact()
{
    if (num_removed > 0)
    {
        // Create a new sequence excluding removed strings.
        StringSequence new_seq;
        new_seq.buffer.reserve(buffer.size());
        new_seq.start.reserve(start.size());
        for (size_t j=0; j < range(); ++j)
        {
            const char* str = (*this)[j];
            if (occupied(j))
                new_seq.insert(str);
        }

        // Swap the new sequence with this one.s
        new_seq.swap(*this);
    }

    // Remove excess capacity.
    if (buffer.capacity() > buffer.size())
        std::vector<char>(buffer).swap(buffer);
    if (start.capacity() > start.size())
        std::vector<size_t>(start).swap(start);
}

void StringSequence::swap(StringSequence& other)
{
    buffer.swap(other.buffer);
    start.swap(other.start);
    std::swap(write_pos, other.write_pos);
    std::swap(num_removed, other.num_removed);
}

////////////////////////////////////////////////////////
// Quick demo:
////////////////////////////////////////////////////////
#include "StringSequence.hpp"
#include <iostream>

using namespace std;

int main()
{
    StringSequence seq;
    seq.insert("foo");
    seq.insert("bar");
    seq.insert("baz");
    seq.insert("hello");
    seq.insert("world");
    seq.erase(2);
    seq.erase(3);

    cout << "Before compaction:" << endl;
    for (size_t j=0; j < seq.range(); ++j)
    {
        if (seq.occupied(j))
            cout << j << ": " << seq[j] << endl;
    }
    cout << endl;

    cout << "After compaction:" << endl;
    seq.compact();
    for (size_t j=0; j < seq.range(); ++j)
    {
        if (seq.occupied(j))
            cout << j << ": " << seq[j] << endl;
    }
    cout << endl;
}

输出:

Before compaction:
0: foo
1: bar
4: world

After compaction:
0: foo
1: bar
2: world

我没有打算让它符合标准(太懒了,结果对于这种特殊情况来说不一定非常有用)但希望这里没有强烈的需求。

答案 1 :(得分:2)

如果要在调试模式下进行编译,则向量的大小取决于实现。通常它至少是一些内部指针的大小(开始,存储结束和保留内存结束)。在我的Linux系统上,sizeof(vector<int32_t>)是24个字节(每个指针大概3 x 8个字节)。这意味着,对于你的10000000件物品,它至少应该是ca. 240 MB。

答案 2 :(得分:2)

长度为vector<uint32_t>的{​​{1}}需要多少内存?以下是一些估算值:

  1. 1的4个字节。这就是你的期望。

  2. CA。 8/16字节动态内存分配开销。人们总是忘记uint32_t实现必须记住分配的大小,以及一些额外的内务处理数据。通常,您可以预期两个指针的开销,因此32位系统上为8个字节,64位系统上为16个字节。

  3. CA。用于对齐填充的4/12字节。必须为任何数据类型对齐动态分配。需要多少取决于CPU,典型的对齐要求是8个字节(完全对齐new)或16个字节(对于CPU向量指令)。因此,您的double实现将为4字节的有效负载添加4/12填充字节。

  4. CA。 new对象本身的12/24字节。 vector<>对象需要存储三个指针大小的东西:指向动态内存的指针,它的大小和实际使用的对象的数量。与指针大小4/8相乘,得到它的sizeof。

  5. 总结一下,我得到vector<>字节,用于存储4个字节。

    从你的数字来看,我猜你是在32位模式下编译的,你的最大对齐是8个字节。在调试模式下,4 + 8/16 + 4/12 + 12/24 = 28/48实现似乎会增加额外的分配开销以捕获常见的编程错误。

答案 3 :(得分:1)

您正在创建41943040vector<int32_t>个实例,存储在另一个vector内。我确信所有实例的720MB都是合理的内存量。内部数据成员加外部向量缓冲区。

答案 4 :(得分:1)

正如其他人所指出的,sizeof(vector<int32_t>)足以在初始化41943040个实例时产生这样的数字。

您可能需要的是cpp字典实现 - 地图: https://www.moderncplusplus.com/map/

它仍然会很大(甚至更大),但风格不那么尴尬。现在,如果记忆是一个问题,那么就不要使用它。

sizeof(std::map<std::string, std::string>) == 48 on my system.