Queue and stack visualizer

A queue hands elements back in the order they arrived, a stack in reverse. Both are usually an array with an index or two, and the details decide what each operation costs. Push and pop through four layouts, count every write and copy, then see how the choice between first-in and last-in plays out on a server that is overloaded.

Elements
3
Capacity
4
Work per operation
–
Structure
Operation
4 slots in a ring: elements leave at the head and join at the tail
1 0 2 1 3 2 3 headtail3 of 4slots used
Order they will leave in
1›2›3
Totals since the start
Operations
0
Slot writes
0
Copies when growing
0
Work per operation
–
  1. –Each step is listed here: writes, wrap-arounds, and the copying when the buffer is full.

How to use the simulator

Pick a structure and add or remove elements. The picture shows the memory the structure really uses: the slots of the array, which of them hold elements, and where the head, tail or top is. The log names every step, and the totals count slot writes, elements copied when the array grows, and elements moved between the two stacks.

  • Enqueue until the ring buffer is full: the next enqueue allocates twice the space and copies everything, unwrapping the elements so the head is at slot 0.
  • Dequeue a few and enqueue again: the tail wraps round to slot 0 and reuses the space at the front, with nothing shifted.
  • In the queue made from two stacks, enqueue several and then dequeue once: every element moves to the out stack in one go. The next dequeues move nothing.
  • Press "20 random" a few times and watch work per operation settle at a small constant, however large the occasional copy.

What are queues and stacks?

A queue is a collection where elements are added at one end, the tail, and removed from the other, the head, so they leave in the order they arrived: first in, first out, or FIFO. A stack adds and removes at the same end, the top, so the newest element always leaves first: last in, first out, or LIFO. Both offer only these operations plus a look at the next element, and both can do them in constant time.

A deque (double-ended queue) allows both at either end, so it can serve as either. A priority queue keeps the same interface but always removes the element with the smallest key; it is a different structure underneath, usually a binary heap.

How they are stored

A stack is the simplest: an array and the index of the next free slot. A push writes there and increments the index; a pop decrements it and reads.

A queue in a plain array would have to shift every element when the head is removed, or leave unused space at the front forever. A ring buffer, also called a circular buffer, avoids both: the head and tail indices advance through the array and wrap round to slot 0 at the end, so freed space at the front is reused. A deque's head can also step backwards, wrapping to the last slot. With a capacity that is a power of two, the wrap is a bitwise AND instead of a division, which is why many implementations, Linux's kfifo among them, round the size up to one.

When the array is full it must be replaced by a larger one, copying every element. That one operation costs O(n), but if the capacity grows by a constant factor the copies are rare enough to average out. Elements copied per element added, over n pushes onto a stack that starts with 4 slots:

PushesDoublingGrowing by half
160.752.00
1,0001.022.13
1,000,0001.052.10

With doubling, the copies form a geometric series, 4 + 8 + 16 + … , that adds up to less than twice the final size, so each push costs O(1) amortized; the exact figure depends on where n falls between two powers of two. Growing by half copies about twice as much but wastes less memory after a resize. Java's ArrayList grows by half; many C++ standard libraries double.

A queue from two stacks

A queue can be built from two stacks. New elements are pushed on the in stack. Removals pop from the out stack, and when it is empty the whole in stack is popped onto it first, which reverses the order so the oldest element ends up on top.

Each element is pushed on the in stack once, moved once and popped from the out stack once, so the average cost per operation is constant. The cost is lumpy: after n enqueues, the first dequeue moves all n elements, and the next n − 1 move none. Moves between the stacks, over n enqueues followed by n dequeues:

ElementsMoves per elementLargest single dequeue
16116 moves
1,00011,000 moves
1,000,00011,000,000 moves

The structure matters in functional programming, where lists are immutable stacks: the two-list queue is the standard purely functional queue, analysed by Chris Okasaki in Purely Functional Data Structures, which also shows how to spread the reversal out to make every operation O(1) in the worst case.

FIFO and LIFO under overload

A server's request queue is where the choice of order has real consequences. FIFO looks fair, but when requests arrive faster than they can be served, the queue grows without limit and every request waits behind all the older ones. Clients give up long before their turn, and the server spends its time answering requests nobody is waiting for any more.

Ben Maurer's 2015 article "Fail at Scale" describes two fixes Facebook uses. A queue timeout in the style of CoDel limits how long a request may wait: if the queue has not been empty at any time in the last 100 ms, new requests may wait only 5 ms, otherwise 100 ms, and requests that run out of time are rejected without doing the work. Adaptive LIFO serves requests in FIFO order normally but switches to LIFO once a queue builds up, so the newest requests, whose clients are still waiting, are served first.

The simulation below has one server with a mean service time of 1 ms (exponentially distributed), requests arriving at random, and clients that give up after 100 ms. Adaptive LIFO here switches when the queue has not been empty for 100 ms, the same signal the timeout uses. Share of all requests answered in time:

Service order80% load95% load110% load130% load
FIFO100.0%100.0%2.8%1.2%
LIFO99.6%96.7%88.8%76.7%
Adaptive LIFO99.9%97.2%88.8%76.7%
FIFO + queue timeout99.5%95.8%87.4%75.7%
Adaptive LIFO + queue timeout99.5%96.2%87.6%75.7%

Below capacity every order does well, and FIFO is best: at 95% load it answers everything in time, while the queue timeout rejects a few requests that would have made it. Above capacity FIFO collapses to almost nothing, because by the time any request reaches the front it is older than 100 ms. The others answer about as many requests as the server has capacity for. In more detail at 110% load:

Service orderIn timeWork wasted on late answersRejected by timeoutMedian99th percentile
FIFO2.8%97.3%0.0%34.4 ms97.6 ms
LIFO88.8%11.1%0.0%2.1 ms69.6 ms
Adaptive LIFO88.8%11.1%0.0%2.1 ms69.5 ms
FIFO + queue timeout87.4%0.0%12.6%9.4 ms43.5 ms
Adaptive LIFO + queue timeout87.6%0.1%12.4%6.1 ms80.2 ms

Latencies are for the answers that arrived in time. LIFO alone still wastes work: requests that were pushed down the stack are eventually served, long after their clients left. The timeout rejects those quickly instead, and adaptive LIFO with the timeout keeps the median low as well.

Overload is often a burst rather than a steady state. Here a server normally at 70% load receives twice its capacity for one second. Answers delivered in time, per second, with the arrival rate in grey:

FIFOcapacity, 1,000 a second0 s1 s2 s3 s4 s5 sAdaptive LIFOcapacity, 1,000 a second0 s1 s2 s3 s4 s5 sFIFO + queue timeoutcapacity, 1,000 a second0 s1 s2 s3 s4 s5 s
The shaded band is the burst. FIFO answers nothing in time for 4.25 s, long after the burst is over, while it works through a backlog of requests whose clients have gone. Over the whole run it answers 26.5% in time, against 81.7% for adaptive LIFO.

As a check on the simulation, with no deadline it reproduces the textbook M/M/1 queue: at 80% load the mean time in the system should be 1 / (1 − 0.8) = 5 ms, and it measured 4.93 ms for FIFO and 4.93 ms for LIFO. The mean does not depend on the order, since the server does the same work either way; the spread does: the standard deviation was 4.9 ms for FIFO and 13.3 ms for LIFO, which is why LIFO alone is a poor default.

Where they are used

  • The call stack. Every function call pushes a frame with its arguments, locals and return address, and returning pops it; recursion depth is stack depth.
  • Parsers and interpreters. Matching brackets, evaluating expressions in postfix order and running stack-based virtual machines such as the JVM and CPython's bytecode interpreter.
  • Graph search. Breadth-first search keeps its frontier in a queue, depth-first search in a stack; swapping one for the other changes the order nodes are visited and nothing else.
  • Channels and kernel buffers. A buffered Go channel is a ring buffer with send and receive indices, and Linux's kfifo is a lock-free ring buffer for one producer and one consumer.
  • High-throughput messaging. The LMAX Disruptor passes events between threads through a preallocated ring buffer with sequence numbers instead of a locked queue.
  • Library deques. Java's ArrayDeque is a resizable ring buffer; Python's collections.deque is a doubly linked list of fixed-size blocks, so it never copies elements to grow.

History

Alan Turing's 1946 design for the ACE computer used a stack of return addresses for subroutines, with instructions he called BURY and UNBURY. Friedrich Ludwig Bauer and Klaus Samelson patented the "Kellerprinzip" (cellar principle) in 1957, using a stack to translate arithmetic formulas, and it went into the design of ALGOL 60 compilers. The study of queues as waiting lines is older: Agner Krarup Erlang analysed telephone exchanges as queues from 1909, founding queueing theory, and the M/M/1 notation used above is David Kendall's, from 1953.

Queue and stack questions and answers

What is the difference between a queue and a stack?

The order things come back out. A queue is first in, first out (FIFO): the element that has waited longest leaves next, like a line at a counter. A stack is last in, first out (LIFO): the newest element leaves first, like a pile of plates. Queues suit work that should be handled in arrival order; stacks suit nested work, such as function calls, undo history and depth-first search.

How is a queue implemented efficiently?

Usually as a ring buffer: an array with a head index where elements leave and a tail index where they arrive, both wrapping around to the start when they reach the end. Enqueue and dequeue are one write or read and an index update, with no shifting. When the array is full it is replaced by a bigger one and the elements are copied across; doubling keeps that copying to about one copy per element on average.

How do you make a queue from two stacks?

Push new elements on an "in" stack. To dequeue, pop from an "out" stack; when the out stack is empty, first pop everything off the in stack and push it on the out stack, which reverses the order so the oldest element is on top. Each element is moved exactly once, so the average cost is constant (1 move per element here), but a single dequeue can move every element: 1,000,000 at once after 1,000,000 enqueues.

Why use LIFO instead of FIFO for request queues?

Because under overload FIFO serves the requests whose clients are most likely to have given up already. In this page's simulation of a server receiving 10% more requests than it can handle, with clients that wait 100 ms, FIFO answered 2.8% of requests in time and spent 97.3% of its work on answers nobody was waiting for; LIFO answered 88.8% in time. Facebook's servers switch to LIFO only once a queue starts to form, which keeps normal traffic fair.

What is a deque?

A double-ended queue: elements can be added and removed at both ends in constant time. It is usually a ring buffer whose head can also step backwards, or a list of fixed-size blocks, as in Python’s collections.deque. A deque can act as both a queue and a stack, and work-stealing schedulers give each worker one: the owner takes work from one end and idle workers steal from the other.

Is a priority queue a queue?

It has the same operations, insert and remove, but removes the element with the smallest (or largest) key instead of the oldest. It is usually built as a binary heap, where both operations take O(log n). Schedulers, Dijkstra’s shortest-path algorithm and timer wheels use priority queues.

Further reading

Published Updated