118. The research frontier
Viscosity says no to a lot of ordinary programs, and it tells you why. Some of those refusals are chores nobody has done yet. Others are questions nobody has answered. Learning to tell them apart is how research starts.
- Read Viscosity’s refusals and sort them into engineering gaps and open problems.
- Explain why a compiler that must prove safety refuses things a run-time system can try.
- Describe three open problems in automatic data layout, with the research that has attacked them.
- Measure which refusals are most common, the first step in choosing what to work on.
Where the map ends
Take a perfectly ordinary program: make 1,000 particles, sort them by
x, then update them in a loop. Ask Viscosity about it:
ps (line 1) {x, y} → kept as array of
objects
· unsafe: line 4: uses .sort on the
array (only .length and .push are
supported)
· unsafe: the array is passed to a
method call at line 4
No speed-up. Swap two elements by hand instead of sorting and it refuses again. Use a class instead of object literals, export the array, or give half the records an extra field, and you get the same answer. Viscosity is careful, and careful means it says no a lot.
Every one of those refusals is a place where the map ends. Some are edges because nobody has walked that far yet. Others are edges because nobody knows the way.
Old maps left blank space where the explorers hadn’t been. Some of that space was just a valley nobody had bothered to walk into. Some of it was an ocean nobody could cross with the ships they had. From the coast, both look the same: blank.
Six edges
Here are six short programs at the edges, with what Viscosity really says about each (all output from the CLI, wrapped):
Two of them it transforms anyway, but not fully. The nested record
gets a column of objects for pos, so reading pos.x still follows a
pointer. The unknown size gets no estimate at all, and the transform
is a bet on n.
Gaps and open problems
The class case is a gap. Recognizing that new P(i) builds an
object with fields x and vx, in that order, is known technique.
Someone has to write the lowering, the shape inference and the tests.
Destructuring (for (const { x } of ps)) is the same kind of thing:
today it is “code Viscosity doesn’t model”.
The others need more than work. Sorting needs every column permuted in step, plus a comparator that receives objects that no longer exist. Varying shapes need either many layouts or a way to say “this field is sometimes missing” in a column. Exported arrays need an analysis of code in other files, some of which may never be shown to the compiler.
An open problem is a question in a field that nobody has a satisfying general answer to yet, as opposed to an engineering gap, where the technique is known and only the work is missing. Telling them apart matters: a gap needs time, an open problem needs an idea, and often a new kind of evidence.
Try one edge yourself. This program is refused because half its
records have a tag. Give every record a tag (an empty string will
do) and watch the verdict change:
Problem one: proof or speculation
Viscosity runs before the program, so it must prove that nobody can tell the difference. A JIT runs during the program, so it can guess and recover. That is exactly the split in the research this book started from. Makor and colleagues turned arrays of objects into columns at run time inside GraalJS, paying for the conversion and earning it back on later accesses[1]. The follow-up duplicated loops so each copy could assume one layout, and transformed nested objects into columns of their own[2].
So a run-time system can try layouts that Viscosity can’t prove safe, while Viscosity can explain its choices before you ship and needs no special engine. Getting both, an ahead-of-time compiler with a cheap escape hatch for the cases it can’t prove, is the open question. Emitting both versions of a loop and choosing at run time is the obvious idea, and it costs code size and a check on every entry.
Problem two: knowing when it pays
The cost model is the least proven part of Viscosity. It skips
collections under 64 elements and otherwise trusts that a loop over a
few fields will win. This book’s own measurements show how rough that
is. In the microbenchmarks, gains shrank to about 1.3–2.3× at 1,000
elements, and AoSoA was often slower than plain objects there
(FINDINGS.md, section 3). In this book’s end-to-end benchmark run
(chapter 111), nbody gained 1.09× while ledger gained 5.11×. In CPython the same layout lost on time
and won on memory (section 6). And SoA loses badly whenever whole
objects must be rebuilt[5]. Viscosity has no model
that predicts all of that for any program on any machine, and this
book knows of none.
Problem three: who decides
Some languages hand the choice to you. In Odin, #soa[N]Vector3 stores
an array as columns while you keep writing v[0].x[4].
ISPC’s soa<N> types do the same for blocked layouts[3].
Viscosity’s @viscosity soa annotation (chapter 102) sits in between:
you ask, and it still checks safety. Whether an automatic tool can
match an expert’s choice, and how it should explain itself when it
disagrees with one, is open.
Sometimes the engine closes an edge for you. In this book’s
microbenchmarks, a SoA loop that built a small object per element and
passed it to a function did not lose to objects: V8 inlined the
function and removed the temporary object by escape analysis
(FINDINGS.md, section 2). A static tool can’t count on that, because
V8 decides it while the program runs. Even the
reports had rough edges: for ps[0] = ps[1], explain used to say “the
array is stored into an array at line 5”, when the array was really the
target of the store. The decision was right; the sentence was wrong.
Now the reason names the operand: “the array is written with an index
store at line 5”.
Measuring the frontier
How do you choose which edge to work on? The same way Part VII chose
its optimizations: measure. Run viscosity explain over a corpus of
real programs and count the reasons. The refusal that blocks the most
programs is the one worth an idea. That is what the exercise builds.
Your turn
Write tally(report). report is the text of a viscosity explain report. Return { soa, aosoa, kept, unsafe }: how many collections were turned into struct of arrays, how many into AoSoA, how many were kept, and how many unsafe: reasons there are. A verdict line contains → followed by struct of arrays, AoSoA, blocks of N or kept as array of objects. An unsafe reason line starts with two spaces, ·, a space and unsafe:.
function tally(report) {
const t = { soa: 0, aosoa: 0,
kept: 0, unsafe: 0 };
for (const l of report.split('\n')) {
if (l.includes('struct')) t.soa++;
}
return t;
}The run-time research handles arrays whose layout can differ by checking which one it has and picking a loop. Write sumX(ps), which returns the sum of the x fields. ps is either an ordinary array of objects, or a SoA container { length, x } whose x is a Float64Array. The container’s column may be longer than length (spare capacity): only the first length entries are elements.
function sumX(ps) {
let s = 0;
for (const p of ps) s += p.x;
return s;
}What you learned
- Viscosity’s refusals mark the edge of what it can prove. Its explain report names each one.
- An engineering gap (classes, destructuring) needs work. An open problem (reordering, varying shapes, whole programs, nested records, unknown sizes) needs an idea.
- Proof or speculation: run-time systems can guess and recover; ahead-of-time ones must prove. Combining the two is open.
- The cost model is a heuristic, and the book’s own benchmarks show how far “when does it pay” is from solved.
- To choose a problem, measure which refusals are common.
Check your understanding
0/6- 1.
Viscosity refuses
ps.sort(...). Which kind of edge is that? - 2.
Arrays of class instances are refused today. Why does the chapter call that a gap, not an open problem?
- 3.
What can a run-time transformation inside a JIT do that Viscosity can’t?
- 4.
Which observation from this book’s measurements shows the cost model is still a heuristic?
- 5.
For
ps[0] = ps[1], explain used to say “the array is stored into an array at line 5”. That was… - 6.
How does a language like Odin approach SoA?
References
- [1] Lukas Makor, Sebastian Kloibhofer, David Leopoldseder, Daniele Bonetta, Lukas Stadler, Hanspeter Mössenböck (2022). Automatic Array Transformation to Columnar Storage at Run Time. Proceedings of the 19th International Conference on Managed Programming Languages and Runtimes (MPLR ’22), ACM, pp. 16–28. doi:10.1145/3546918.3546919 (checked 2026-10-31)
- [2] Sebastian Kloibhofer, Lukas Makor, David Leopoldseder, Daniele Bonetta, Lukas Stadler, Hanspeter Mössenböck (2023). Control Flow Duplication for Columnar Arrays in a Dynamic Compiler. The Art, Science, and Engineering of Programming, vol. 7, issue 3, article 9. doi:10.22152/programming-journal.org/2023/7/9 (checked 2026-10-31)
- [3] The Intel ISPC project. Intel ISPC User’s Guide: Structure of Array Types. ispc.github.io documentation. ispc.github.io/ispc.html (checked 2026-10-31)
- [4] The Odin project (odin-lang.org documentation contributors). Overview: SOA data types. Odin programming language documentation, odin-lang.org. odin-lang.org/docs/overview/#soa-data-types (checked 2026-10-31)
- [5] Bui Huy Giap (“Zap”, ziap) (2025). Struct of Arrays (SoA) in TypeScript with metaprogramming. Zap’s website (blog post, 17 September 2025). ziap.github.io/blog/parallel-array/ (checked 2026-10-31)