Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is Deque (ArrayDeque) capacity a power of two?

In Java (but similarly in PHP) the ArrayDeque implementation always has its capacity as a power of 2:

http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/classes/java/util/ArrayDeque.java#l126

For HashMap this choice is clear - to have a uniform element distribution based on a trimmed 32-bit hash. But Deque inserts/removes elements sequentially.

Also, ArrayList doesn't restrict its capacity to a power of two, just ensures it's at least the number of elements.

So, why does the Deque implementation require its capacity to be a power of 2?

like image 408
radistao Avatar asked Jun 26 '19 19:06

radistao


People also ask

What is the difference between deque and ArrayDeque?

Deque Interface: It is a Doubly Ended Queue in which you can insert the elements from both sides. It is an interface that implements the Queue. ArrayDeque implements both Queue and Deque. It is dynamically resizable from both sides.

What is the difference between queue and deque in Java?

Queue Interface: It is an Interface that is a FirstIn – FirstOut Data Structure where the elements are added from the back. Deque Interface: It is a Doubly Ended Queue in which you can insert the elements from both sides. It is an interface that implements the Queue. ArrayDeque implements both Queue and Deque.

What are the limitations of ArrayDeque?

Array deques have no capacity restrictions and they grow as necessary to support usage. They are not thread-safe which means that in the absence of external synchronization, ArrayDeque does not support concurrent access by multiple threads. Null elements are prohibited in the ArrayDeque.

How to add an element to ArrayDeque in Java?

Let us go through each of the operations by implementing alongside by providing clean java program as follows: In order to add an element to the ArrayDeque, we can use the methods add (), addFirst (), addLast (), offer (), offerFirst (), offerLast () methods.


2 Answers

Finally i found it!!! The reason not just in performance and bits-mask operations (yes, they are faster, but not significantly). The real reason is to allow loop back the elements capacity if we use sequential adding-removing operations. In other words: reuse released cells after remove() operation.

Consider the following examples (initial capacity is 16):

  1. Only add():

    1. add 15 elements => head=0, tail=15

    2. add more 5 elements => doubleCapacity() => head=0, tail=20, capacity=32

    enter image description here

  2. add()-remove()-add():

    1. add 15 elements => head=0, tail=15

    2. remove 10 elements => tail loops back to removed indexes => head=10, tail=15

    3. add more 5 elements => the capacity remains 16, the elements[] array is not rebuilt or reallocated! => new elements are added into the place of the removed elements to the beginning of the array => head=10, tail=4 (looped back to the start of the array from 15->0->1->2->3->4). Note the values 16-19 are inserted to the indexes 0-3

    enter image description here

So, in this case using power of two and concise bit operations makes much more sense. With such approach the operations like if ( (tail = (tail + 1) & (elements.length - 1)) == head) allow to assign and verify easily that the looped tail does not overlap with the head (yeah, the stupid snake where actually the tail bites the head :) )

The code snippet to play around:

ArrayDeque<String> q = new ArrayDeque<>(15); // capacity is 16

// add 15 elements
q.add("0"); q.add("1"); q.add("2"); q.add("3"); q.add("4");
q.add("5"); q.add("6"); q.add("7"); q.add("8"); q.add("9");
q.add("10"); q.add("11");q.add("12");q.add("13");q.add("14");

// remove 10 elements from the head => tail LOOPS BACK in the elements[]
q.poll();q.poll();q.poll();q.poll();q.poll();q.poll();q.poll();q.poll();q.poll();q.poll();

// add 5 elements => the elements[] is not reallocated!
q.add("15");q.add("16");q.add("17");q.add("18");q.add("19");


q.poll();
like image 172
radistao Avatar answered Oct 07 '22 03:10

radistao


I guess, for performance reasons. For example, let's look at implementation of addLast function:

public void addLast(E e) {
    if (e == null)
        throw new NullPointerException();
    elements[tail] = e;
    if ( (tail = (tail + 1) & (elements.length - 1)) == head)
        doubleCapacity();
}

So, instead of tail = (tail + 1) % elements.length it is possible to write tail = (tail + 1) & (elements.length - 1) (& works faster, than %). Such constructions are used many times in ArrayDeque's source code.

like image 5
Leo Leontev Avatar answered Oct 07 '22 04:10

Leo Leontev