Hardware Engineering · Storage Hardware
Channels, Dies, and Planes: How a Flash Controller Reaches Its Throughput
A single flash die serves one operation at a time, so drive throughput comes from concurrency across channels and planes; the arithmetic of that concurrency sets the queue depth a workload needs.
A flash die is slow. Reading a page takes tens of microseconds, programming one takes hundreds, erasing a block takes milliseconds. Those numbers have not improved much across generations, because they are set by the physics of moving charge through oxide rather than by lithography. What has improved is how many operations a drive performs at once, and the entire architecture of a solid-state drive is a machine for turning one slow die into many slow dies working in parallel.
One die does one thing at a time
Inside a die there is one array, one row decoder, one set of sense amplifiers, and one page buffer. A read operation latches a page from the array into the buffer, and the buffer is then read out over the die’s interface. A program operation latches data from the interface into the buffer and then drives it into the array. An erase operates on a block.
The array cannot serve a read and a program simultaneously: both need the row decoder and the high-voltage circuits, and the sense amplifiers used for verify during programming are the same ones used for reading. So a die processes one array operation at a time. Its throughput in isolation is the reciprocal of the operation’s latency — tens of thousands of reads per second, a few thousand programs, a few hundred erases.
That number is far below what a drive is expected to deliver, and the gap is closed by concurrency at three levels: multiple planes within a die, multiple dies sharing a channel, and multiple channels.
Channels, chip enables, and the shared bus
A channel is a bundle of data, control, and status signals connecting the controller to a group of dies. The dies on a channel share the data bus and are selected individually by chip-enable lines. Only one die drives the bus at a time, so the bus is a shared resource that must be arbitrated.
This creates a distinction that the scheduler has to respect: the array operation and the data transfer are separate phases, and the bus is busy only during the transfer. A read has three phases — command and address out, array access, data out — and the middle phase occupies the die but not the bus. The controller can therefore issue a read to one die, let it access the array, and use the bus to transfer data from a different die that has finished its access. Interleaving dies on a channel is what keeps the bus from being the bottleneck.
The bus width and rate set the ceiling on the transfer phase. The common interfaces use an 8-bit data path with double-data-rate signalling, at rates that have risen from tens of megatransfers per second to the high hundreds across generations. At 8 bits and a few hundred megatransfers per second, a channel moves on the order of hundreds of megabytes per second, which is comparable to the array’s own read rate for a single die — so a channel with several dies must interleave carefully, or transfers will queue behind each other and the dies will idle.
Planes and multi-plane commands
A die is not one array but several. The array is split into planes, each with its own page buffer and its own sense amplifiers, sharing the row decoder in a way that lets them be addressed together. A multi-plane command programs or reads one page in each plane with a single command sequence, and the planes’ array operations proceed in parallel.
Multi-plane operation is why the effective page size of a part is often double its single-plane page size: the controller treats a plane pair as one logical unit with an interleaved address, so a 16 KiB page on each of two planes is issued as one 32 KiB operation. The programming time for the pair is only slightly longer than for a single plane, so the per-byte cost falls.
Planes also define the granularity at which the controller can spread a request. A request that is smaller than the multi-plane unit cannot use the full width of the die, which is one reason small random writes achieve lower throughput than sequential ones even when the array is otherwise idle.
The arithmetic of concurrency
The relationship between concurrency and throughput is Little’s law: the number of operations in flight equals the arrival rate multiplied by the time each spends in the system. Rearranged, throughput equals concurrency divided by latency.
For a drive with 8 channels and 8 dies per channel — 64 dies — each capable of one read at a time with a latency of tens of microseconds, the ceiling is 64 divided by that latency: on the order of a million reads per second. The figure is a bound, not a measurement; the achieved rate is lower because the bus, the controller’s own processing, and the error-correction stage all add work that is not parallel with the array access.
Writes have a much lower ceiling for the same structure, because a program takes several times longer than a read. A drive with the same 64 dies might sustain on the order of a hundred thousand programs per second, and the ratio between read and write throughput in a specification follows from the ratio between the two latencies, as long as the bus is not the bottleneck.
The queue depth needed to reach the ceiling follows from the same law. If a single request takes tens of microseconds and the drive can hold 64 in flight, the host must have at least 64 outstanding requests to keep every die busy. A host that issues one request and waits for it gets the latency of one die, not the throughput of 64. This is why the interface’s queue depth matters and why benchmarks that report high throughput at a stated queue depth are reporting a property of the queue, not of the device alone.
# Little's law applied to a die array. Latencies are order-of-magnitude values.
def ceiling_iops(dies, latency_us):
return dies / (latency_us * 1e-6)
def needed_depth(iops_target, latency_us):
return iops_target * latency_us * 1e-6
dies = 8 * 8 # channels x dies per channel
print(f"read ceiling ~{ceiling_iops(dies, 40):,.0f} IOPS")
print(f"write ceiling ~{ceiling_iops(dies, 400):,.0f} IOPS")
print(f"depth for 200k IOPS at 40 us: {needed_depth(200_000, 40):.0f}")
The last line is the useful one for capacity planning: a target throughput implies a required queue depth, and a host that cannot generate that depth will not reach the target no matter how many dies the drive contains.
Scheduling across an asymmetric array
The controller’s scheduler has more freedom than the arithmetic suggests, and it uses it. Requests arrive as logical addresses; the mapping table says which physical page holds each; the scheduler groups pages that live on different dies and issues them together, so that a single host request of a few kilobytes is split into chunks that land on separate dies.
The chunk size is a design parameter. Too small, and the per-command overhead dominates; too large, and the request cannot be spread across enough dies. A common choice aligns the chunk to the multi-plane page size, which is the natural unit the array can service in parallel.
Two effects degrade the ideal. The first is garbage collection, which consumes die time without serving a host request. A collection cycle that copies many pages competes with host reads for the same array resources, and the drive’s internal scheduling must decide how much of the array to give it. The second is the distribution of the mapping itself: if the controller allocates physical pages by a hash of the logical address that does not spread uniformly, some dies receive more traffic than others, and the drive’s throughput is set by the busiest die rather than by the average. Wear levelling and interleaving policy are therefore the same problem viewed from two directions — spreading writes evenly across dies is good for endurance and good for throughput, and a policy that optimises one at the expense of the other shows up as either a short-lived drive or a slow one.
What the parallelism does to latency
Concurrency raises throughput without lowering the latency of an individual operation. A read still takes tens of microseconds from command to data, and no amount of queue depth changes that. What changes is the distribution: with many requests in flight, some complete quickly because their die was idle, and some wait behind other requests on a busy die. The latency distribution widens at the tail while the mean stays close to the single-operation figure.
That tail is the number that matters for a synchronous workload. A database that issues one read and waits pays the single-operation latency plus whatever queueing delay its own concurrency creates. A workload with deep queues pays the mean but experiences the tail only when a request happens to land behind a long queue on one die. The two workloads see the same drive and report latencies that differ by an order of magnitude, and neither is wrong.
The controller’s queue depth also bounds how much it can hide. When the host’s queue is shallower than the die count, the drive cannot fill the array, and the throughput it reports is a property of the host’s concurrency. This is why a device that performs well in a benchmark with a deep queue can appear slow in a single-threaded application, and why the queue depth is part of any throughput figure worth quoting. The same principle governs the mechanical case, though for a different reason: a disk reorders to reduce travel, while a flash drive reorders to fill idle dies, and the two mechanisms are described together in seek time, rotational latency, and the cost of queueing.
References
- Open NAND Flash Interface Specification — ONFI Working Group
- Inside Solid State Drives (SSDs) — R. Micheloni, A. Marelli, K. Eshghi, Springer
- NVM Express Base Specification — NVM Express
- Flash Memories — P. Cappelletti, C. Golla, P. Olivo, E. Zanoni (eds.), Kluwer Academic Publishers, 1999