skip to content

A decoder body is written once against a placeholder sample type: which two strategies can a toolchain use to make it run?

level: middleimportance: must knowfreq 58%

answer

  1. two ways to make generic code run
  2. many copies or one body
  3. who supplies the type's operations
  4. direct inlinable call versus table entry
  5. build-time copies versus run-time indirection

basics

~20 s

A toolchain can generate a separate copy of the body for each type argument, with every operation resolved to a direct call, or compile one shared copy that receives the type's operations as a passed-in table and calls them indirectly.

solid answer

~40 s

Two strategies. **Generate a body per type argument**: the build emits one copy of the decoder for every sample type it sees, substitutes that type throughout, and each copy knows the layout, so every operation becomes a direct call the optimiser can inline. **Keep one shared body**: the decoder is compiled once against a representation it does not know and receives the type's operations as a passed-in table, so each operation is an indirect call through that table. The first pays at build time — one compilation and one emitted copy per argument — and the second pays once per operation while running. The same source compiles either way: which one you get is a property of the platform, not of the program text.

code

pseudocode · 20 lines
pseudocode
// one generic body, written once
function decodeAll(window, T):
    out = empty list
    for each slot in window:
        out.append(T.read(slot))        // an operation of the placeholder type
    return out

// strategy A - the build emits one copy per type argument
function decodeAll_for_status(window):
    out = empty list
    for each slot in window:
        out.append(read_status(slot))   // direct call, can be inlined
    return out

// strategy B - one body, operations arrive in a table
function decodeAll_shared(window, ops):
    out = empty list
    for each slot in window:
        out.append(ops.read(slot))      // indirect call through ops
    return out

go deeper

for a junior

Recall that the same generic body can end up as many compiled copies or as one shared copy, and that something has to tell a shared copy how to operate on the value it is handed.

for a middle

Explain how each strategy reaches the type's operations: substitution and direct calls inside a generated copy, an entry in a passed-in table in a shared one. Say where each strategy sends the bill.

for a senior

Show you can work out which strategy a platform used from what is observable: build time scaling with the number of argument types, duplicated near-identical bodies, or an argument type accepted that the build never saw.

for a principal

Treat the choice as a distribution decision. Generated copies bind consumers to compiling your generic bodies; a shared body fixes a per-operation indirection into every artifact you publish.

A telemetry decoder on a field controller is written once: it walks a byte window and turns each slot into a sample of some placeholder type `T`, calling a small set of operations on `T` — read a value out of a slot, report how wide it is, decide whether it is valid. The body names no concrete sample type. Machine instructions cannot be that vague: they need a width, a field offset, and the address of the code implementing each operation. Something between the source and the running controller has to supply what the body left open, and there are two families of answer. Real platforms also mix them. ## Generating a body per type argument The build finds every distinct type argument the program uses the decoder with and emits a separate compiled body for each, with the placeholder replaced throughout. This is **monomorphisation**: many single-type bodies replace one many-type body. - Inside each copy the argument is concrete, so widths, offsets and alignments become constants baked into instructions. - Every operation has a known target, so the call is direct and the optimiser may inline it, fold its result, hoist an invariant check out of the loop, or vectorise the loop around it. - Each copy can be optimised differently — the copy for a one-byte status word and the copy for a timestamped pair need not end up looking alike. - The set of copies is fixed by what the build could see. A type argument that shows up later has no copy, and none appears without another compiler run. ## Keeping one shared body The alternative compiles the decoder exactly once, against a representation it does not know, and hands it the type's behaviour as data: a **table of operations** for the argument in use — read, width, validity. The body calls through that table. - One body is emitted no matter how many argument types exist. - The body either handles values through a uniform, fixed-size representation it can copy without understanding, or receives their size and layout alongside the operations. What it cannot do is bake a width into its instructions. - Each operation is an indirect call: load the entry, jump to whatever address is there. - The table is chosen **outside** the body. Where a call site knows the concrete argument, the compiler selects the table there and passes it in; where the argument is only known later, the table travels with the value. - A type argument invented after the body was compiled works, because nothing in the body was ever specialised to a type. ## Where each one pays | | one body per argument | one shared body | |---|---|---| | work at build time | one compilation per distinct argument | one compilation in total | | emitted code | one copy per argument | one copy | | cost per operation | direct call, inlinable | indirect call through the table | | new argument type after the build | needs another copy generated | works unchanged | | what a distribution must contain | the body in re-compilable form | a finished binary | Neither column is a verdict. On a controller with a hot per-sample loop and three known sample types, the left column is close to free and buys real speed; on one that must accept sample types from modules loaded in the field, the right column is the only column that works. ## The strategy belongs to the platform, not the source The same decoder source compiles either way. Some platforms pick one for everything; some let an individual declaration be marked for generation while the rest stay shared; some emit a shared body and let a run-time optimiser produce a specialised copy after observing that the same table keeps arriving. That is why the question is usually phrased as *which one did your platform choose* — the answer is not visible in the program text. You can usually tell from outside: 1. Build time that grows with the number of distinct argument types rather than with the amount of source points to generation. 2. Many near-identical bodies in the emitted image, differing only in baked-in widths, point to generation. 3. One body serving every argument, with small tables of operations near the call sites, points to the shared strategy. 4. A type argument accepted that the build could not have seen settles it: only a shared body can do that. ## Why this gets asked It is the cleanest test of whether a candidate understands that *generic* is a source-level idea with more than one realisation underneath. Someone who knows one platform well will often assert that generic code simply *is* copies, or simply *is* one shared body, and be surprised the other exists. The answer that lands names both, says who supplies the type's operations under each, and puts the bill in the right place: at build time and in image size on one side, at every operation on the other.

  • Where does the operations table come from in the shared-body strategy?
    From outside the body. When the call site knows the concrete argument type, the compiler selects the table there and passes it in beside the value. When the type is only known later — a value handed in by a module loaded at run time, say — the table travels with the value. The body never picks a table; it only calls through the one it was given.
  • Can a single program use both strategies at once?
    Yes. Several toolchains generate copies for a marked or heavily used set of arguments and fall back to one shared body for the rest, and a run-time optimiser can emit a specialised copy of a hot shared body after the fact. The two are ends of a range, not an either/or that must be settled program-wide.
  • Does the shared body need to know how wide a value of the placeholder type is?
    It needs that from somewhere. Either every value travels in a uniform fixed-size representation the body can move without understanding it, or the size and layout are passed in alongside the operations. What the shared body cannot do is bake a width into its instructions — which is precisely what each generated copy does.

A publisher can print a separate edition for every language it anticipates — fast to read, but useless for a language nobody printed — or send one interpreter who works from whichever phrasebook is handed over at the door, which covers any language at the price of a lookup per sentence.

saying these in an interview costs you the question

  • Says there is only one way: generic code always becomes copies
  • Thinks a shared body still knows the concrete argument type inside it
  • Believes the shared body finds its own operations table by inspecting the value
  • Says the compilation strategy is stated in the program's source
  • Assumes generating copies is free because it happens at build time