skip to content

A mapper resolves each type's members once into a prepared plan — which per-field cost does that remove, and which survives?

level: seniorimportance: should knowfreq 45%

answer

  1. split the cost by what it depends on
  2. type-dependent work can be paid once
  3. access-dependent work cannot
  4. objects must outnumber types
  5. the plan is memory you keep forever

basics

~20 s

A prepared plan removes the part of the cost that depends only on the type: matching names to members, enumerating the member set, and, in designs that record it, the access decision. What survives is per-access work — packing values generically and calling an unknown target.

solid answer

~50 s

The trick is to split the cost by what it depends on. Resolving a name to a member depends on the **type**, so it can be done once and kept: the mapper builds an ordered plan of resolved members for that type, keyed by the type, and every later object of that type walks the plan instead of searching. That turns a cost of fields times objects into fields once per type. What no plan can remove is the work that depends on the **access**: each value still moves through the generic representation, each call still goes through an entry point whose target the optimizer cannot pin down. So the planned path is dramatically cheaper than the naive one and still measurably slower than compiled field access — and the plan itself now costs memory that grows with the number of distinct types the process ever sees.

code

pseudocode · 9 lines
pseudocode
plan = planCache.get(typeOf(source))
if plan is missing
    plan = empty list
    for each member in describeMembers(typeOf(source))   // paid once per type
        plan.append(resolve(member))
    planCache.put(typeOf(source), plan)

for each entry in plan                                   // paid per object
    target.write(entry.slot, entry.read(source))         // still generic, still indirect

go deeper

for a junior

Recall that the expensive part of resolving a member by name can often be done once and kept, instead of being repeated for every object.

for a middle

Explain the split: work that depends only on the type is cacheable, work that depends on each access is not, and say which phases fall on each side.

for a senior

Read the profile and prove the cache is working — resolution appearing once per type and then vanishing — and name what the plan costs in memory and first-use latency.

for a principal

Decide where the plan lives and who owns its lifetime across services, and set the rule for when an open-ended set of planned types is unacceptable.

## What the plan is A **prepared plan** is the result of asking the expensive questions once. For a given type, the mapper walks the type's description, resolves each field it cares about into a member it can hold onto, works out the conversion each one needs, and stores that ordered list against the type. Every later object of that type is mapped by walking the list. The idea generalises past mapping: anywhere a program resolves something from a name at run time, the resolution can usually be separated from the use, and only the use has to repeat. ## Split the cost by what it depends on This is the whole analysis, and it is what an interviewer is listening for: - Work that depends only on the **type** — enumerating members, matching names, deciding conversions, and in designs that record it, the access decision — is paid **once per type**. - Work that depends on the **access** — moving the value through the generic representation, allocating an argument container, the indirect call — is paid **every time**. | Cost | Without a plan | With a plan | |---|---|---| | Enumerating the member set | Per object | Once per type | | Matching a name to a member | Per field per object | Once per field per type | | Deciding access | Per access, or once at resolution | Once at resolution where the design records it | | Packing values generically | Per field per object | Per field per object | | Indirect, unpinnable call | Per field per object | Per field per object | ## What a plan never removes The planned path is still a **generic** path. It holds members, not compiled offsets, so each read and write still moves its value through one uniform representation, still wraps values that are not reference-shaped, and still enters through a call the optimizer cannot resolve to a single body. A candidate who says the plan closes the gap to compiled access entirely has stopped one step early; the honest claim is that it removes the part that grows with type width and leaves the part that grows with traffic. ## Where the plan itself costs you 1. **Memory that never shrinks.** One plan per distinct type, held so it can be reused, means footprint grows with the number of types the process ever maps. For a service that maps a bounded set of types this is nothing; for one that maps types supplied by data it is a leak with a respectable name. 2. **First-use latency.** Building plans lazily does not remove the resolution cost, it **relocates** it. The first object of each type pays it, which turns a startup cost into a tail-latency spike spread across the run. Building at startup pays the same total up front and predictably. Neither is free; choose which shape you want. 3. **Invalidation, where the model allows it.** In models where a type's member set can change while the program runs, a held plan can describe a type that no longer looks like that, so the cache needs a way to be invalidated. Where types are fixed once loaded, this concern does not arise. ## How to tell the plan is actually working The useful signal is in the profile, not in the code review: - Resolution should appear **once per distinct type** and then disappear. If it keeps reappearing, the cache is being missed — the key is finer than the type, or plans are being built per instance, per request, or per mapper object rather than shared. - Steady-state cost should be dominated by packing and the generic call. If it is still dominated by name matching, the plan is not on the hot path at all. - Footprint should plateau. If plan memory keeps climbing after warm-up, the set of types being planned is open rather than bounded, and that is worth knowing before it becomes an incident. ## The judgment to carry away A plan is worth building when a type is mapped many times and worth very little when each type is mapped once or twice, because then you pay resolution and never amortise it. That is the shape of the trade: the plan converts a per-object cost into a per-type cost plus permanent memory, and it pays exactly to the degree that objects outnumber types.

  • Where does the cost go when plans are built lazily on first use instead of at startup?
    It relocates rather than disappears. The first object of each type pays for that type's resolution, so a predictable startup cost becomes a set of latency spikes spread through the run. Total work is the same; only its distribution and who notices it change.
  • What still makes the planned path slower than compiled field access?
    Every access still moves its value through the generic representation, often allocating to do so, and still enters through a call whose target the optimizer cannot pin to one body. Those costs depend on the access, not the type, so no per-type caching touches them.
  • When does a per-type plan stop paying for itself?
    When types are numerous and each is mapped once or twice, so resolution is paid and never amortised, or when the cache is keyed more finely than the type, so plans are rebuilt instead of reused. Both show up as resolution that never stops appearing in the profile.

saying these in an interview costs you the question

  • Says a cached plan makes the path as fast as compiled field access
  • Builds the plan per object or per request instead of once per type
  • Assumes an unbounded plan cache is free because entries look small
  • Thinks the plan removes the generic packing done on every access
  • Believes building plans lazily removes the cost rather than relocating it