Why is deque using so much more RAM than vector in

2019-06-16 21:24发布

I have a problem I am working on where I need to use some sort of 2 dimensional array. The array is fixed width (four columns), but I need to create extra rows on the fly.

To do this, I have been using vectors of vectors, and I have been using some nested loops that contain this:

array.push_back(vector<float>(4));
array[n][0] = a;
array[n][1] = b;
array[n][2] = c;
array[n][3] = d;
n++

to add the rows and their contents. The trouble is that I appear to be running out of memory with the number of elements I was trying to create, so I reduced the number that I was using. But then I started reading about deque, and thought it would allow me to use more memory because it doesn't have to be contiguous. I changed all mentions of "vector" to "deque", in this loop, as well as all declarations. But then it appeared that I ran out of memory again, this time with even with the reduced number of rows.

I looked at how much memory my code is using, and when I am using deque, the memory rises steadily to above 2GB, and the program closes soon after, even when using the smaller number of rows. I'm not sure exactly where in this loop it is when it runs out of memory.

When I use vectors, the memory usage (for the same number of rows) is still under 1GB, even when the loop exits. It then goes on to a similar loop where more rows are added, still only reaching about 1.4GB.

So my question is. Is this normal for deque to use more than twice the memory of vector, or am I making an erroneous assumption in thinking I can just replace the word "vector" with "deque" in the declarations/initializations and the above code?

Thanks in advance.

I'm using: MS Visual C++ 2010 (32-bit) Windows 7 (64-bit)

5条回答
等我变得足够好
2楼-- · 2019-06-16 21:40

It all depends on the internal implementation of deque (I won't speak about vector since it is relatively straightforward).

Fact is, deque has completely different guarantees than vector (the most important one being that it supports O(1) insertion at both ends while vector only supports O(1) insertion at the back). This in turn means the internal structures managed by deque have to be more complex than vector.

To allow that, a typical deque implementation will split its memory in several non-contiguous blocks. But each individual memory block has a fixed overhead to allow the memory management to work (eg. whatever the size of the block, the system may need another 16 or 32 bytes or whatever in addition, just for bookkeeping). Since, contrary to a vector, a deque requires many small, independent blocks, the overhead stacks up which can explain the difference you see. Also note that those individual memory blocks need to be managed (maybe in separate structures?), which probably means some (or a lot of) additional overhead too.

As for a way to solve your problem, you could try what @BasileStarynkevitch suggested in the comments, this will indeed reduce your memory usage but it will get you only so far because at some point you'll still run out of memory. And what if you try to run your program on a machine that only has 256MB RAM? Any other solution which goal is to reduce your memory footprint while still trying to keep all your data in memory will suffer from the same problems.

A proper solution when handling large datasets like yours would be to adapt your algorithms and data structures in order to be able to handle small partitions at a time of your whole dataset, and load/save those partitions as needed in order to make room for the other partitions. Unfortunately since it probably means disk access, it also means a big drop in performance but hey, you can't eat the cake and have it too.

查看更多
劳资没心,怎么记你
3楼-- · 2019-06-16 21:43

The real answer here has little to do with the core data structure. The answer is that MSVC's implementation of std::deque is especially awful and degenerates to an array of pointers to individual elements, rather than the array of arrays it should be. Frankly, only twice the memory use of vector is surprising. If you had a better implementation of deque you'd get better results.

查看更多
趁早两清
4楼-- · 2019-06-16 21:46

Deque can have additional memory overhead over vector because it's made of a few blocks instead of contiguous one.

From en.cppreference.com/w/cpp/container/deque:

As opposed to std::vector, the elements of a deque are not stored contiguously: typical implementations use a sequence of individually allocated fixed-size arrays.

查看更多
唯我独甜
5楼-- · 2019-06-16 21:51

Theory


There two common ways to efficiently implement a deque: either with a modified dynamic array or with a doubly linked list.

The modified dynamic array uses is basically a dynamic array that can grow from both ends, sometimes called array deques. These array deques have all the properties of a dynamic array, such as constant-time random access, good locality of reference, and inefficient insertion/removal in the middle, with the addition of amortized constant-time insertion/removal at both ends, instead of just one end.

There are several implementations of modified dynamic array:

  1. Allocating deque contents from the center of the underlying array, and resizing the underlying array when either end is reached. This approach may require more frequent resizings and waste more space, particularly when elements are only inserted at one end.

  2. Storing deque contents in a circular buffer, and only resizing when the buffer becomes full. This decreases the frequency of resizings.

  3. Storing contents in multiple smaller arrays, allocating additional arrays at the beginning or end as needed. Indexing is implemented by keeping a dynamic array containing pointers to each of the smaller arrays.

Conclusion


Different libraries may implement deques in different ways, but generally as a modified dynamic array. Most likely your standard library uses the approach #1 to implement std::deque, and since you append elements only from one end, you ultimately waste a lot of space. For that reason, it makes an illusion that std::deque takes up more space than usual std::vector.

Furthermore, if std::deque would be implemented as doubly-linked list, that would result in a waste of space too since each element would need to accommodate 2 pointers in addition to your custom data.

Implementation with approach #3 (modified dynamic array approach too) would again result in a waste of space to accommodate additional metadata such as pointers to all those small arrays.

In any case, std::deque is less efficient in terms of storage than plain old std::vector. Without knowing what do you want to achieve I cannot confidently suggest which data structure do you need. However, it seems like you don't even know what deques are for, therefore, what you really want in your situation is std::vector. Deques, in general, have different application.

查看更多
Luminary・发光体
6楼-- · 2019-06-16 21:51

The primary issue is running out of memory.

So, do you need all the data in memory at once?
You may never be able to accomplish this.

Partial Processing

You may want to consider processing the data into "chunks" or smaller sub-matrices. For example, using the standard rectangular grid:

  • Read data of first quadrant.
  • Process data of first quandrant.
  • Store results (in a file) of first quandrant.
  • Repeat for remaining quandrants.

Searching

If you are searching for a particle or a set of datum, you can do that without reading in the entire data set into memory.

  1. Allocate a block (array) of memory.
  2. Read a portion of the data into this block of memory.
  3. Search the block of data.
  4. Repeat steps 2 and 3 until the data is found.

Streaming Data

If your application is receiving the raw data from an input source (other than a file), you will want to store the data for later processing.

This will require more than one buffer and is more efficient using at least two threads of execution.

The Reading Thread will be reading data into a buffer until the buffer is full. When the buffer is full, it will read data into another empty one.

The Writing Thread will initially wait until either the first read buffer is full or the read operation is finished. Next, the Writing Thread takes data out of the read buffer and writes to a file. The Write Thread then starts writing from the next read buffer.

This technique is called Double Buffering or Multiple Buffering.

Sparse Data

If there is a lot of zero or unused data in the matrix, you should try using Sparse Matrices. Essentially, this is a list of structures that hold the data's coordinates and the value. This also works when most of the data is a common value other than zero. This saves a lot of memory space; but costs a little bit more execution time.

Data Compression

You could also change your algorithms to use data compression. The idea here is to store the data location, value and the number or contiguous equal values (a.k.a. runs). So instead of storing 100 consecutive data points of the same value, you would store the starting position (of the run), the value, and 100 as the quantity. This saves a lot of space, but requires more processing time when accessing the data.

Memory Mapped File

There are libraries that can treat a file as memory. Essentially, they read in a "page" of the file into memory. When the requests go out of the "page", they read in another page. All this is performed "behind the scenes". All you need to do is treat the file like memory.

Summary

Arrays and deques are not your primary issue, quantity of data is. Your primary issue can be resolved by processing small pieces of data at a time, compressing the data storage, or treating the data in the file as memory. If you are trying to process streaming data, don't. Ideally, streaming data should be placed into a file and then processed later. A historical purpose of a file is to contain data that doesn't fit into memory.

查看更多
登录 后发表回答