对于您的情况:
所以,这是否意味着向量的向量存储小不是一个好主意
对象? ——
一般不会。嵌套的sub-vector 并不是存储大量可变大小序列的好解决方案。例如,您不想使用每个多边形单独的 std::vector 实例来表示允许可变多边形(三角形、四边形、五边形、六边形、n 边形)的索引网格,否则您会倾向于炸毁内存使用并有一个非常慢的解决方案:慢,因为每个单独的多边形都涉及堆分配,并且在内存中爆炸,因为向量除了以通常大于需要的方式存储大小和容量之外,还经常为元素预分配一些内存如果你有一堆很小的序列。
vector 是一种出色的数据结构,可以连续存储一百万个事物,但对于存储一百万个极小的向量来说就不是那么出色了。
在这种情况下,即使是单链索引列表也可以更好地使用指向更大向量的索引,执行速度更快,有时甚至使用更少的内存,尽管有 32 位链接开销,如下所示:
也就是说,对于您的特殊情况,我建议您使用大量可变长度字符串的随机访问序列:
// 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];
如果您还需要搜索功能,您实际上可以存储大量字符串并快速搜索它们(包括基于前缀的搜索),存储的内存甚至比使用 compressed trie 的上述解决方案还要少.虽然这是一个涉及更多的解决方案,但如果您的软件围绕字符串字典并搜索它们并且您可能只能找到一些已经为您提供的第三方库,那么它可能是值得的。
std::string
为了完整起见,我想我会提到std::string。最近的实现通常通过预先存储不单独堆分配的缓冲区来优化小字符串。但是,在您的情况下,这可能会导致更大的内存使用量,因为这会使 sizeof(string) 更大,消耗的内存远远超过真正短字符串所需的内存。它确实使std::string 对临时字符串更有用,因此如果您像这样从那个大的字符向量中提取std::string,那么您可能会得到性能非常好的东西:
std::string str = strings.data() + string_start[n];
...而不是:
const char* str = strings.data() + string_start[n];
也就是说,字符的大向量在存储所有字符串时会在性能和内存方面做得更好。一般而言,如果您想存储数百万个小容器,任何类型的通用容器往往都不会表现得如此出色。
主要的概念问题是,当需要一百万个可变大小的序列时,需求的可变大小性质与容器的通用性质相结合将意味着您拥有一百万个很小的内存管理器,所有这些必须潜在地在堆上分配,或者,如果不是,分配比需要更多的数据,以及跟踪其大小/容量(如果它是连续的,等等)。不可避免地,超过 100 万个拥有自己记忆的管理器会变得非常昂贵。
因此,在这些情况下,放弃“完整、独立”容器的便利性而使用一个巨大的缓冲区或一个存储元素数据的巨型容器(如 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
我没有费心让它符合标准(太懒了,结果不一定对这种特殊情况有用得多),但希望这里不是一个强烈的需求。