Wordclock — a German word clock (Wortuhr)

Measured decisions

What is written down here was settled by building the image both ways rather than by argument, and has no header that owns it: it is about a pattern spread over the whole tree, not about one class. The numbers describe the change that was made and are therefore history — they were true when measured, and updating them would make them a lie. The platform contract states the rule that came out of it; this is the measurement behind the rule.

The Fast accessor pair

Every platform accessor used to come in two forms: one that validated its index and answered StdReturnType, one that trusted the caller and answered the value. The justification was real for this target — a freestanding 8-bit part built with -Os and a loop over 110 pixels per frame, where a bounds check per pixel is time rather than a formality.

What put it in doubt is that getOutputPixel existed for its whole life with only the Fast half, on all three backends, and nothing noticed. A pattern in two parts whose second part nothing enforces loses its second part.

The justification did not survive the measurement. The bounds check was put inside the platform seam’s Fast forms, so that all three hundred call sites paid for it without one of them being rewritten, and the AVR image was built either way:

  text
as it was 48 210
with the seam checking every access 48 114

The checked build was 96 bytes smaller, and the reason is worth more than the number. Per symbol, Pixels::setPixelFast grew by 66 bytes and then stopped being inlined, which took 48 bytes off Display::setPixel, 46 off Display::setPixelFast, 42 off AnimationMatrix::setTimeTask and 48 off the RPC dispatcher. So what the difference measured is an inlining threshold, not the cost of a comparison — and a flash argument that turns on the inliner’s mood is not an argument.

Two things the measurement turned up that nobody had anticipated.

The pair was invisible to the linker almost everywhere. Of 45 declared pairs, 35 appeared in the image as neither half — fully inlined or dropped — and only three (getPixel, setPixel, writePixel) existed as both. For most of the pattern there was no runtime object to be cheap or expensive about; it was upkeep and nothing else.

And in the core, Fast did not mean unchecked. Display::setPixel(Column, Row) validated nothing itself: it computed an index and handed it to Pixels::setPixel, which is where the only check in the chain lived. The two halves in Display differed in whether the caller was told, not in whether anything was verified — so half the pattern was not a safety mechanism at all.

The time argument, which no host can measure, comes out negligible by arithmetic. The frame path is 110 accesses, and render() returns early unless the buffer is dirty, so the worst case is a display changing on every 10 ms tick: 11 000 checks a second. At a generous four cycles each that is 44 000 of 24 000 000, 0.18 % of the part, and a clock showing a settled face pays none of it.

What replaced it

The check moved into the single implementation and the name went with the pattern. There is one accessor per operation now, and where a reader has two forms the argument list says which — getPixel(Index, Pixel) answers a code, getPixel(Index) answers the pixel, and both check. Writers have one form, which answers; a caller with nothing to do with that answer ignores it. 440 mentions across the core and all four backends, and the image came out 586 bytes smaller — 48 664 before, 48 078 after, RAM unchanged to the byte.

Six things it settled, and they are the reason the shape is what it is: