Step 3 — Array Element Kinds (and an Unexplained Result)

Goal

Measure array storage specialisation, and practise reporting a result you could not fully explain.

Predict first

5,000,000-element arrays summed with an identical operation. Rank:

PACKED_SMI       all small integers
PACKED_DOUBLE    all doubles
PACKED_ELEMENTS  1% nulls mixed in
HOLEY_SMI        1% holes

Run

npm run arrays

Expected output:

  element kind                          | median ms | ns/elem | vs SMI
  PACKED_SMI      all small integers    |      7.80 |   1.560 |  1.00x
  PACKED_DOUBLE   all doubles           |      4.70 |   0.940 |  0.60x
  PACKED_ELEMENTS 1% nulls mixed in     |      7.70 |   1.540 |  0.99x
  HOLEY_SMI       1% holes, no strings  |      8.00 |   1.600 |  1.03x

  Spread across all four kinds: 1.70x

What just happened

V8 specialises array storage by content. PACKED_SMI stores tagged 31-bit integers; PACKED_DOUBLE stores unboxed doubles; PACKED_ELEMENTS stores general tagged values. Transitions are one-way — adding one null to a numeric array changes storage for the whole array, and removing it does not restore the fast kind.

And PACKED_DOUBLE measured faster than PACKED_SMI, which is backwards.

This is the point of the step. The first hypothesis was that the accumulator overflowed SMI range (summing raw i over 5M elements reaches ~1.25e13) and transitioned to a double mid-loop — attributing to the array a cost that belonged to the sum variable. That was testable: constraining values with i % 100 keeps the sum near 2.5e8, well inside SMI range.

The fix was applied. The ordering did not change. Hypothesis rejected.

The most plausible remaining explanation is that SMI addition carries a per-add overflow check that double addition does not. It was not investigated further, because the entire effect is ~0.6 ns/element, and Step 4 shows what that is worth.

The skill being practised

Recording "reproduced, one hypothesis tested and rejected, not pursued because the magnitude does not justify it" is a better professional output than either a confident wrong explanation or silence. It is logged as an open question, not as a finding.

Knowing when to stop investigating is the same judgment as knowing when to stop optimising. This is a safe place to practise it: the stakes are 0.6 nanoseconds.

Checkpoint

docs/verification.md Checkpoint 3 — including that reproducing the oddity is a pass.

Going deeper (optional)

If you want to settle it, node --allow-natives-syntax --print-opt-code and compare the generated loops. If you do settle it, update docs/measured-results.md §3 and the learning log — that is a genuine contribution to this repository.