
Double-ended vector – is it useful? - ingve
http://larshagencpp.github.io/blog/2016/05/22/devector
======
twotwotwo
Qt's QList is like what this describes: it tracks where the first and last
used items are in the allocated region, so both prepends and appends are
amortized constant time.

Peeking at the code, Qt's even using the same general approach described in
the post ("When there is still a lot of room left in the buffer, we should
move elements toward the middle, and not reallocate straight away."): it moves
items into the middle if the backing array is under 1/3 full.

When QList first came out, I think the team said they got there by trying
different implementations and measuring what worked best on average in the
real app code they had. That at least suggests that to the author's question
"is it useful?", somebody thought yes.

(The code I peeked at is at
[https://github.com/radekp/qt/blob/master/src/corelib/tools/q...](https://github.com/radekp/qt/blob/master/src/corelib/tools/qlist.cpp),
header at
[https://github.com/radekp/qt/blob/master/src/corelib/tools/q...](https://github.com/radekp/qt/blob/master/src/corelib/tools/qlist.h),
and docs (possibly for a newer version) at
[http://doc.qt.io/qt-5/qlist.html#prepend.](http://doc.qt.io/qt-5/qlist.html#prepend.))

------
agf
This is an interesting article, and a "devector" is something I've never heard
of or considered before, so I'll leave critique of the data structure itself
to others. But I have some constructive criticism of how the data is
presented. Since the graphs / performance comparisons are such a large part of
the post, I think it's relevant.

First of all, and most simply, when presenting multiple charts please use the
same colors for the same lines on each chart. Here, the charts without the
"vector" line use a different color scheme, making it harder to compare them
at a glance.

Second, absolute timings don't matter here, only relative timings do, so
showing absolute time on the Y-axis doesn't really make sense. Instead, I
would use percentage from a baseline of one of the data structures. I think
choosing the "deque" line for that purpose would make the most sense, given
that it's the standard data structure from double-ended access.

Here's a rough version of what that looks like for the first chart --
[https://goo.gl/bg5mrN](https://goo.gl/bg5mrN) \-- Hopefully it makes the
relative speeds of the different solutions more clear (I only put in the data
for "deque" and "devector").

~~~
ktRolster
_Second, absolute timings don 't matter here, only relative timings do, so
showing absolute time on the Y-axis doesn't really make sense_

I like to see the real timings, though. It helps me get a sense for how long
real-world tasks take. In this one, we can see that things were happening on
the order of nanoseconds.

~~~
agf
The primary purpose of the graph is to show relative timings, so I think it's
important to make that clear. A logarithmic Y-Axis makes that _really_ hard to
judge.

If you wanted to show absolute timings, then I would say a separate graph with
time / N on they Y-axis would be the right way to do it -- you could see how
the time per operation changed as you increased N.

~~~
ktRolster
_A logarithmic Y-Axis makes that really hard to judge._

Come on, we're programmers, get good at math!

~~~
agf
Thanks, I'm plenty good at math. The point of a graph is to display
information visually -- minimal math should be required.

If what you want is the numbers, then a graph doesn't help you -- use a table
instead.

~~~
ktRolster
Pretty clearly your "understand graphs with logarithmic axes" could use some
work.

------
nkurz
I feel like it would be possible to leave the heavy lifting to the underlying
page tables. Presuming you are running a 64-bit system, you have a enormous
practically unused address space. Until you access the allocated memory it
doesn't actually occupy any physical RAM.

The virtual address gets associated with the physical RAM in 4KB pages. So if
you just allocate an a region twice as large as you will ever need, and start
writing in the middle, you will never be wasting more than 8KB of real RAM. No
copying, no reallocation, just keep track somewhere of the head and tail.

Other than the momentary panic of those who notice how much memory is being
"used", what are the downsides of this approach? The 8KB is unlikely to ever
be a problem. I haven't seen many people taking this approach to memory
management, and other than stigma, I'm not sure why. Is it that everyone still
wants to support 32-bit systems?

~~~
omgtehlion
Even further: allocate the same physical region twice. Please google what
"virtual ring buffer" is ))

~~~
glandium
If only that were possible without a file...

~~~
omgtehlion
It is completely possible without a file (on linux, winNT, osx). Syscall names
might mislead you, but this is how it is...

------
chillacy
It might be of interest to mention that std::deque is implemented as a linked
list of arrays: [http://cpp-tip-of-the-day.blogspot.com/2013/11/how-is-
stddeq...](http://cpp-tip-of-the-day.blogspot.com/2013/11/how-is-stddeque-
implemented.html)

~~~
gpderetta
Note that std::deque is not really implemented as a linked list, as it would
prevent O(1) random access. The classical implementation is as a dynamic array
of pointers to fixed size chunks.

~~~
IshKebab
That's what he just said...

~~~
jjnoakes
"linked list of arrays"...

"Dynamic array of fixed size chunks..."

They don't look the same to me. They do to you?

------
nightcracker
I have already made this concept a year ago:
[https://github.com/orlp/devector](https://github.com/orlp/devector).

I never finished the implementation though. Exception safe programming of
standard containers is incredibly annoying (feel free to inspect the source
code to see what I mean).

------
clarkd99
I have made a circular queue where data can be pushed or popped on either end
of a vector of structs.

Adding an entry can be done on the end or the head without moving any other
nodes. All that is required is to have an index for the head and one for the
tail. Used as a queue, you would push nodes on the tail and pop nodes off the
head. You could just as easily add nodes to the head and pop them off the tail
or any combination that you like. You could iterate over the list by starting
at the tail and moving backward toward the head. You could also start at the
head and move forward toward the tail. If you try to move below the first
element then set the index to the last entry. If you try to move above the
last entry then continue on the front of the vector.

If the head and tail have the same number then no entries are in the vector.

If the buffer size (which determines how many entries you can have) is about
to be exceeded then create a bigger buffer and copy the old entries to the new
buffer.

Instead of creating a bigger buffer and copying all the entries, you could
make a new "cluster" of the same size as the current buffer and then you could
use an integer division and modulo to determine in which cluster and what
offset any node might be at.

Using the cluster method, the whole struct could grow as needed, and never
move any existing data entries while pushing/popping data at either the head
or tail of the list of array nodes. The struct could be just a single number
or any other sized structure. Memory allocations would be minimized by
allocing a buffer of "n" times the size of each node.

------
ajuc
Interesting. I needed a similar data structure once and ended up using linked
list (but it had exactly $VERTICAL_RESOLUTION big elements so the memory cost
for poitners was negligible, and the most common operation was moving all
elements left(up) or right(down) and linked list is hard to beat at that).

------
chmike
This is definitely useful because it gives us a double ended queue with
efficient random access. I can't think of a use case right now, but I'm sure
there are some.

------
mrpopo
Interesting implementation. Given that it seems to perform more moves than a
regular vector, I'm wondering how much more efficient this would be if it
could just memmove the whole block around (AFAIK, the spec doesn't allow it
because of constructors/destructors for complex structures).

------
Someone
Isn't that a std::deque
([http://en.cppreference.com/w/cpp/container/deque](http://en.cppreference.com/w/cpp/container/deque))?

~~~
deathanatos
I don't think std::deque's implementation is specified in standard. It only
needs to meet certain running times, and the data structure in the article —
the author's "devector" — would satisfy the requirements of std::deque; i.e.,
the "devector" is a valid implementation of std::deque, I think. However, in
practice, std::deque is not implemented this way — your link hints at this:

> _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._

The usual implementation, I think, looks something like the image here[1].

[1]:
[http://stackoverflow.com/a/6292437/101999](http://stackoverflow.com/a/6292437/101999)

~~~
nightcracker
> the "devector" is a valid implementation of std::deque, I think

It is not. std::deque has strict requirements that say appends/prepends may
not invalidate references/iterators. A devector can not guarantee this.

~~~
foota
Nitpick, the operation seems to be allowed to invalidate iterators, though not
references. "An insertion in the middle of the deque invalidates all the
iterators and references to elements of the deque. An insertion at either end
of the deque invalidates all the iterators to the deque, but has no effect on
the validity of references to elements of the deque" [1] There might be some
cases where it would be practical to simply not free the previously used
memory, such that after N increases in size there are N copies of each
element. This would only roughly double the space required.

[1] C++ working draft, 11 megabytes; [http://www.open-
std.org/jtc1/sc22/wg21/docs/papers/2014/n429...](http://www.open-
std.org/jtc1/sc22/wg21/docs/papers/2014/n4296.pdf)

~~~
nightcracker
Ah, I thought it was both, but was just working from the top of my memory.
Good for looking it up :)

------
foota
Wow, that's a really neat data structure!

------
thomasahle
This is known as a Circular Buffer
[https://en.m.wikipedia.org/wiki/Circular_buffer](https://en.m.wikipedia.org/wiki/Circular_buffer)

In Java it is ArrayDeque
[https://docs.oracle.com/javase/8/docs/api/java/util/ArrayDeq...](https://docs.oracle.com/javase/8/docs/api/java/util/ArrayDeque.html)

~~~
chmike
It really is a double ended queue. A circular buffer is a particular data
structure that share some properties with the double ended queue.

One of the difference is that a circular buffer has a fixed capacity. The
double ended queue has no capacity limit.

~~~
clarkd99
A circular buffer can contain a number of nodes and get bigger when it runs
out of space by making a larger buffer and copying the old data or clustering
as I described above. It doesn't have a "fixed capacity".

My circular queue works without moving any elements on insertion (with cluster
method) or just moving the nodes each time the buffer size is exceeded. It
seems this "double ended queue" isn't as useful because it has to copy nodes,
find a middle etc.

Even in the "cluster" version of my circular queue, you can quickly calculate
the actual location of any node directly using it's "virtual" index number,
even though the head and tail can "float" anywhere along each cluster array of
structs.

------
trakl
I don't understand the phrase "Moving O(n) elements...".

As far as I know, Big O notation is used to compare growth rates of algorithms
as their input size changes.

------
bob8
Can't you have a regular vector and place the place the appended items on the
even indices and prepended items on the odd? If you keep count of how many
prepended and appended items you have you have identical performance to a
regular vector except a worst case doubling of memory.

