mightsleep

Two dimensions, one disk

· 6 min read

This series started as a side question. I had just written an allocator for GPU memory, which packs buffers into one long line, and wondered why the line had to be one-dimensional. Two dimensions led to Morton order, Morton picked up a few ideas from image recognition, and soon I was solving Morton with bit tricks instead of packing anything. The packing can wait.

Take a table with two columns you filter on: a position on a map, or a time and a sensor id. The question is always the same: give me everything inside this rectangle. Memory, a file and an index all have one dimension, so before anything can be stored, someone has to decide in which order the cells of a two-dimensional grid go into a line. This post is about that decision. It opens a short series; the posts after it compute the orders fast and measure what they cost.

Rows

The obvious order is the one every array uses, row after row. On an 8 × 8 grid, cell (x, y) gets number y · 8 + x.

0123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
Row by row, with a 4 × 4 query marked. The line follows the numbers; it jumps back at the end of every row.

The marked square holds numbers 18 to 21, 26 to 29, 34 to 37 and 42 to 45: four contiguous stretches of the index. I will call such a stretch a run. Each run means jumping to a new place in the index and reading from there, so a run is roughly a seek. In this order a rectangle costs one run per row it covers, whatever its width, so a query one column wide and a hundred tall costs a hundred.

That is only half of the cost, though. A disk hands out pages, blocks of consecutive keys, and a database reads them whole. In rows, a page is a strip, a piece of one row. A square query cuts through as many strips as it has rows and uses only the few cells of each strip that fall inside it.

A curve

A space-filling curve is just another order: a path through the grid that visits every cell once, chosen so that cells close in the order are also close on the grid. The name sounds like geometry, but all that matters here is the numbering.

Z-order

The cheapest such order interleaves the bits of the two coordinates like a zipper: the lowest bit of x, then the lowest bit of y, then the next bit of x, and so on. The cell x = 2, y = 1 is x = 10 and y = 01 in binary; interleaved, that is 0110, so the cell gets number 6. This is Z-order, also called Morton order after G. M. Morton, who described it for a map database at IBM in 1966.

0145236789121310111415
Z-order on a 4 × 4 grid. Every 2 × 2 block is a small Z, and the four blocks make a large one.

Every 2 × 2 block gets four consecutive numbers, every aligned 4 × 4 block sixteen, and so on up. That is the whole point: an aligned square of any size is one contiguous stretch of the index. The price is the jumps between blocks, the diagonal strokes of the Z, and some of them are long.

Hilbert

David Hilbert's curve, from 1891, is built from the same blocks, but each block is turned so that the path leaves it right next to where the next block starts. It never jumps: consecutive numbers are always neighbouring cells.

0114153213124781156910
Hilbert's curve on the same grid. Each quarter is a turned copy of the 2 × 2 pattern, so the path never lifts.

It is also harder to compute. Z-order is a bit shuffle, while Hilbert has to know at every level how the block it is in is turned, and that is a chain of decisions, each depending on the last. Making that cheap is the subject of the next post.

What a square costs

Here are both numbers for a setup that looks more like a database than a toy: a 256 × 256 grid, pages of 256 cells, and a 16 × 16 square query, averaged over all 58 081 positions the square can take.

orderrunspages readcells read for 256 wanted
rows16.016.04096
Z-order30.03.7957
Hilbert15.93.7957

Pages are where the curves win, and both win by the same margin. On a curve a page is a block close to a square, because an aligned square of the grid is a contiguous stretch of keys, and a square query cuts through only a few such blocks. Reading a quarter of the data is a real saving.

Pages of 16 cells, outlined: whole rows on the left, 4 × 4 blocks on Hilbert's curve on the right (Z-order's pages are the same blocks). The 8 × 8 query touches the shaded pages, eight against four. It is aligned to the blocks here, which flatters the curve; the table averages every position.

Runs are a different story. Hilbert only ties with plain rows, and Z-order needs almost twice as many: every jump between blocks starts a new run. For a square, a curve does not mean fewer seeks. Whether runs or pages decide what a query really costs, and whether something between Z-order and Hilbert does better, is what the rest of this series measures.

Try it

Below are three orders: Z-order, Hilbert, and one between them that gets a post of its own. Drag a rectangle across the grid; the curve outside it dims and the runs inside stay lit, so you can count them.

Morton (Z-order), Inside-out and Hilbert, computed in Rust compiled to WebAssembly. Drag to select a rectangle; the curve outside it dims, so the runs stay lit.

Where this lives

Geohash is Z-order: the bits of latitude and longitude, interleaved and written in base 32, so that nearby places share a prefix. Delta Lake can sort the files of a table along it, OPTIMIZE ... ZORDER BY, so that a filter on either column skips most files. Google's S2 library numbers cells on the faces of a cube along Hilbert's curve. And GPUs lay textures out in tiles much like Z-order, because a triangle reads a small square of texels, not a row.

Next

Three questions for the posts that follow:

Method

The numbers in the table come from a short script that numbers the grid in each order, collects the keys of every 16 × 16 square, sorts them, and counts runs as gaps between consecutive keys and pages as distinct values of key / 256. The same script draws the figures, so the pictures and the table cannot disagree. Hilbert is the classic construction, the same one the demo uses.