Skip to content

Spend memory to save work in the group walk - #22

Open
Roxxik wants to merge 2 commits into
mist64:masterfrom
Roxxik:layout
Open

Spend memory to save work in the group walk#22
Roxxik wants to merge 2 commits into
mist64:masterfrom
Roxxik:layout

Conversation

@Roxxik

@Roxxik Roxxik commented Jul 30, 2026

Copy link
Copy Markdown

Two changes to how node state is stored, both trading space this program is not
short of for operations it performs tens of millions of times.

A cbmbasic run to READY. takes on the order of ten thousand cache misses in
total, against 2.9 billion cycles. The working set is 89 KB and lives in L2. The
bit-packing in this file was buying space that was never scarce, at the cost of a
shift and a mask on the hottest path in the program.

One byte per node value, and a precomputed base

nodes_value becomes one byte per node. It is read 126 million times per run as
get_nodes_value(c.gate) while scanning transistors, and the whole array is 1725
bytes.

Separately, each node's pullup/pulldown contribution is precomputed into
nodes_base. Pullups and pulldowns only change in setNode() and
writeNodes(), a handful of nodes per half-cycle, yet addNodeToGroup read both
bitmaps on all 80 million group-walk steps. Every update to the running value is
a max, so the three separate compares collapse into one.

Answer group membership by scanning the group

A group was stored twice: as an array plus a count, and as a redundant bitmap
over all 1725 nodes so that group_contains was O(1). Every one of the 79.9
million addNodeToGroup calls a run makes opens with a bitmap test, and every
one that proceeds sets a bit that group_clear later has to clear again.

Set that against what the structure actually holds. Two thirds of the 30 million
groups built are a single node, 98% are three or fewer, and the mean is 1.45.
Four cache lines of bitmap and three operations per node visited were being spent
to answer "is this node one of the one or two I have so far".

Dropping the bitmap makes group_add a single store and group_clear an
assignment. The search runs over one or two entries that were just written. The
walk becomes quadratic in the group size, which is the point: it is quadratic in
1.45. Groups of eight or more occur 132 times in a whole run and the largest is
54 nodes, so a full scan per insertion costs on the order of 200K comparisons
against the 210M bitmap operations removed.

Verification

measure is byte-identical over all 256 opcodes, and cbmbasic reaches READY.
at the same half-cycle with the same end state.

Beyond the architectural state, every one of the 1725 nodes is folded into an
FNV-1a digest every 16 cycles and compared against a master build. Both suites
agree exactly: 3bbda10b350a759f over 17,946 samples of Tom Harte's
SingleStepTests, and 2e3d527522bdd11d over 125,000 samples of the first two
million cycles of Klaus Dormann's functional test. Changing how a value is stored
should not change the value, and this is the check that says it did not. Harte
itself is 67,159 passed, 1,711 unstable on die-dependent opcodes, 0 failed,
unchanged from master.

While developing, a build that kept the bitmap and asserted the two membership
answers agreed on every call ran the same 256 opcodes clean.

Performance

cbmbasic to READY., turbo disabled. Wall clock is the minimum of fifteen
interleaved runs, counters the minimum of five:

master this PR
wall 0.845 s 0.694 s -17.9%
cycles 2,916,838,378 2,412,075,307 -17.3%
instructions 7,121,314,607 4,832,899,885 -32.1%
branch misses 51,068,528 46,172,947 -9.6%

Split between the two commits, on cycles against each one's own parent: the
byte-per-node value and the precomputed base are -9.3%, and dropping the
membership bitmap on top of that is -8.8%.

The price is 3,018 bytes. Allocations go from 88,842 to 91,860: nodes_value
grows from 216 bytes to 1725, nodes_base adds another 1725, and the group
bitmap gives 216 back. The working set comment in setupNodesAndTransistors is
updated from 89 KB to 92 KB.

Note for review

This subsumes #17. group_clear no longer reads groupcount at all, so the
uninitialised read that fix addresses stops existing rather than getting
initialised. valgrind reports it on a master build and is silent on this one.
The two do not conflict textually and either order works.

I used an LLM to help me out in this work, but I manually reviewed all changes made.

Roxxik added 2 commits July 30, 2026 20:08
nodes_value becomes one byte per node instead of a bitmap. It is read
126 million times per cbmbasic run as get_nodes_value(c.gate) while
scanning transistors, and the whole array is 1725 bytes, so the shift
and mask cost more than the space they save. A run to READY. takes on
the order of ten thousand cache misses in total, so the packing was
buying space that was never scarce.

nodes_base caches each node's pullup/pulldown contribution, which
addNodeToGroup previously read from both bitmaps on all 80 million
group-walk steps. Pullups and pulldowns only change in setNode() and
writeNodes(), a handful of nodes per half-cycle. Every update to val is
a max, so the three compares collapse into one.

Allocations go from 88,842 bytes to 92,076: nodes_value grows from 216
bytes to 1725, and nodes_base adds another 1725. The working set comment
is updated to match.

cbmbasic to READY. is 9.3% fewer cycles, minimum of five runs with
turbo disabled. measure is byte-identical over all 256 opcodes.
A group was stored twice: as an array plus a count, and as a redundant
bitmap over all 1725 nodes so that group_contains was O(1). Every one of
the 79.9 million addNodeToGroup calls a cbmbasic run makes opens with a
bitmap test, and every one that proceeds sets a bit that group_clear has
to clear again.

Scanning the array instead makes group_add a single store and
group_clear an assignment. The search runs over one or two entries that
were just written. This is quadratic in the group size, which averages
1.45 and peaks at 54.

cbmbasic to READY. is 8.8% fewer cycles, minimum of five runs with turbo
disabled. measure is byte-identical over all 256 opcodes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant