Memory Management & Memory Mapping
Memory Allocation (q's Heap)
This section covers q's own heap: the memory q allocates for the objects it holds itself, such as a table in memory or a query result. That is separate from the mapped files of the previous section, which q reads through but doesn't own.
How q Allocates Memory
Every time q creates an object, such as a list, a table or a query result, it needs a block of memory to put it in. Asking the OS for memory is slow, so q doesn't do it for every object. Instead it works like this:
- q asks the OS for a big chunk, 64 MB at a time. KX calls this chunk a slab.
- q cuts slabs into blocks whose sizes are powers of two, from 16 bytes up to 64 MB. Each of these sizes is called a size class (KX says "size category"). To make a block of the right size, it cuts a bigger free block in half, and keeps halving until a piece is just big enough.
- Each object gets the smallest block it fits in. An object that needs 33 KB gets a 64 KB block.
- When an object is freed, its block goes onto a free list for its size, ready to be reused by the next object of that size. Reusing a block is much faster than asking the OS again.
Anything larger than 64 MB doesn't fit this scheme, and is requested directly from the OS.
Two more details:
- Splitting blocks in half like this, and merging free halves back together, is a classic technique often called a buddy allocator. KX's documentation doesn't use that name, but it describes the same splitting.
- Each thread has its own heap. In a process with secondary threads, the main thread has one heap and every secondary thread gets its own.
Why Memory Usage Looks Higher Than Expected
Because every allocation up to 64MB rounds up to the next size class, real memory usage is rarely a clean multiple of bytes per element × count:
\ts a:til 4094 / 0 33088
\ts a:til 4095 / 0 65856\ts reports the memory allocated while evaluating the expression, so the number includes the vector's own class plus some small additional housekeeping (such as type and length information), not just the raw element data. The boundary above is the interesting part: a vector of longs fits inside a 32KB class only up to 4094 elements; the 4095th element pushes the whole vector into the 64KB class, roughly doubling the memory used for one extra element's worth of data.
This rounding isn't a bug to work around — it's a deliberate trade-off favouring fast allocation over packing every byte tightly. It does mean memory budgeting should leave headroom beyond the theoretical minimum, especially for vectors sitting close to a size-class boundary.
Per-Object Overhead
The raw data isn't the only thing taking up space — every list carries a small, fixed metadata overhead on top of its elements (covering its type, count, reference count, and any attribute), before the whole thing gets rounded up to its size class. A 2-element long vector, for instance, needs 2×8 bytes of data plus that fixed overhead — already enough to round up past the smallest available class. \ts is a handy way to see this directly: it reports both how long an expression took and how many bytes it allocated.
Why Over-Allocation Isn't Pure Waste
That spare room inside a rounded-up class isn't necessarily wasted — appending to an existing vector that still has headroom in its current class can grow it in place with little extra allocation, right up until the class is full. Building a brand-new vector of the same final size, instead of growing into one, always pays the full allocation cost from scratch. This is one reason accumulating into one vector over time can be cheaper than repeatedly creating fresh ones.
Reassignment and Freeing
a:til 4094
a:til 4095Reassigning a doesn't grow the old object — the name just stops pointing at the old vector and starts pointing at a new one. If nothing else still refers to the old vector, q can free its memory back into the heap immediately. That doesn't mean the operating system sees the process shrink, though: freed memory normally stays inside q's heap, ready for q to reuse quickly, rather than being handed straight back to the OS. Returning memory from q's heap to the OS is a separate, coarser-grained operation, covered in the next section.
Experiment: Confirming the Pattern with .Q.w[]
The same size-class jump repeats at every boundary, and reassigning a variable to a bigger object only changes .Q.w[]'s used figure by the difference between the old and new class — confirming that the old class really is freed immediately, not left counted until later:
Expression (reassigning a) | Old class | New class | used change |
|---|---|---|---|
a:til 4094 | — | 32KB | +32768 (fresh allocation) |
a:til 4095 | 32KB | 64KB | +32768 (64KB − 32KB) |
a:til 16382 | 64KB | 128KB | +65536 (128KB − 64KB) |
a:til 16383 | 128KB | 256KB | +131072 (256KB − 128KB) |
a:til 32766 | 256KB | 256KB | +0 (same class, no jump) |
a:til 32767 | 256KB | 512KB | +262144 (512KB − 256KB) |
.Q.w[]'s heap field, by contrast, stays flat at the process's full reservation throughout this whole sequence — freeing a class's worth of memory stops it counting toward used, but the slab the OS gave the process isn't returned just because one object inside it was freed. That's the coarser step covered in the next section.
A class can genuinely hold far more than an object's data needs, and used reflects the full class, not just the live bytes inside it: til 2050 (≈16KB of actual data) and til 4094 (≈32KB of actual data) both round up to the same 32KB class and add the identical amount to used, even though one is using barely half of what it's been given.
The figures above also include a small, constant cost for binding the name itself: \ts til 4095 (no a:) reports 65712 bytes — 144 less than \ts a:til 4095 — confirming that 144-byte gap is the cost of binding the name a, separate from the vector's own class.
Practice Questions
Test your understanding with 2 hands-on questions