Skip to main content

The optimizer

the optimizer

Fusing stages into one loop removes the plumbing tax. This page is about the optimizer, the part that removes work you wrote but never needed. Its organizing idea: a product (a tuple, a case class (Scala's record type), later a JSON record) is treated as a set of independent columns, and each column is computed at its first read or not at all. Everything below falls out of that idea; every emitted block is a golden test.

Dead-column elimination

A tuple or case class is a set of independent columns. Here map builds three, then the pipeline reads only two of them. The optimizer never builds the tuple: column 2 (x * 13) is read by nobody, so it's dead and absent from the loop; column 0 (x % 3) is needed by the filter, so it's computed eagerly. A View (Scala's runtime-lazy collection wrapper, its analogue of a java.util.stream pipeline) allocates the 3-tuple (plus three boxed Ints) for every element, and throws most of it away.

you write

loading source…

the macro emits

loading source…

Compute-for-survivors: the sink

expensive(x) lands inside the if. Column 1 is needed only by the final map, which sits downstream of the filter, so it's computed only for elements that survive. An expensive column behind a selective filter runs a fraction of the time, an optimization the lazy-collection model can't express, because it has no view of which columns a later stage will read.

you write

loading source…

the macro emits

loading source…

Common-subexpression elimination

x * x is written twice but computed once: bound to one val and reused across both tuple components. Decomposition, dead-column elimination, the sink, and CSE all fall out of a single idea: model a product as independent lazy columns, and bind each at its first use.

you write

loading source…

the macro emits

loading source…

…and the decomposition reaches the fold's lambda

The same column analysis reaches into a foldLeft/reduce lambda (foldLeft is a seeded reduce: it starts from an initial value and folds left to right, so the result type can differ from the element type). Here the fold reads only s.score, so the optimizer never builds the Stat: its other fields (and the object itself) are dead and absent, and the loop is just acc = acc + (x * 100). Without this, the fold would rebuild the entire product per element just to read one field.

you write

loading source…

the macro emits

loading source…

Emitted blocks are the verbatim post-typer expansion (FuseDebug.show), regenerated to current codegen and checked into tests/snapshots/; they are shown, not executed.