The label table is kept in order, and halved instead of walked

labFind walked the index from the front, so every use of every label
cost a scan of every label defined so far, each with a string compare.
The cost grew with the program being built, which is what made it hurt:
assembling CosmOS on the machine took 1,833,691,267 cycles against the
assembler assembling itself at 57,257,133 - three times the source for
thirty two times the time.

Sorted and halved, the same build is 886,498,996. THE WALK WAS 52 PER
CENT OF THE WHOLE ASSEMBLY, which settles a suspicion this project has
carried unverified for weeks and puts a number on it.

Eleven comparisons against two thousand entries where a walk averaged six
hundred and seventy. The search hands back where a name WOULD go, which
is what adding one needs and what a walk could never have offered, so
labAdd gets its insertion point for nothing.

sameText was already an ordering and did not have to change: Q is the
difference at the first character that differed, and the Carry Flag from
that same subtraction survives the return because nothing puts the Status
register back. A name that runs out while the other carries on borrows
against the other's character, which sorts the shorter first.

Small programs pay about a tenth more - 57.3M to 63.5M for the assembler
on itself - because adding a label now moves the tail of the index up and
a short table was never expensive to walk. That is the right way round
for a trade to fall.

numHalve and numBack are new: a rotate right on a CIRCULAR sixteen bit
register brings bit nought back in at the top, so halving means taking
that bit off again, and the low half has to go down first because the
mask wants B.

WHICH END THE TABLE IS SORTED FROM DOES NOT MATTER. labAdd takes its
insertion point from labFind, so the comparison that decides the order is
the same one that searches it - turn it round and the table is built
backwards and read backwards and no output changes. Tests/break.sh says
so, correctly, by not noticing.

Verified by CosmOS builds CosmOS and second generation staying byte
identical. An indexing bug cannot hide behind a fixed point.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
Anachronaut
2026-09-06 21:02:05 -04:00
co-authored by Claude Opus 5
parent 563bc20a75
commit e20c9bac1f
2 changed files with 152 additions and 8 deletions
+113 -8
View File
@@ -1,5 +1,22 @@
; The label table: the only thing that survives between the two passes.
;
; ---- KEPT IN ORDER, so that a lookup can halve it ----
;
; It used to be walked from the front, and that was most of the assembler. Every use of every
; label paid a scan of every label defined so far, so the cost grew with the program being
; built: assembling CosmOS on the machine took 1.83 billion cycles against the assembler's own
; 57 million - three times the source for thirty two times the time.
;
; Sorted and halved instead, it is 886 million: the walk was 52 per cent of the whole
; assembly. Small programs pay about a tenth more, because adding a label now moves the tail
; of the index up to make room and a short table was never expensive to walk. That is the
; right way round for a trade to fall.
;
; WHICH END IT IS SORTED FROM DOES NOT MATTER, and that is not carelessness. labAdd takes its
; insertion point from labFind, so the comparison that decides the order is the same one that
; searches it; turn it round and the table is built backwards and read backwards and nothing
; else can tell. Which is why breaking that comparison on purpose changes no output at all.
;
; Names are packed end to end in an arena and each index entry holds a pointer into it,
; rather than every entry carrying a field wide enough for the longest name. MEASURED on
; CosmOS, which is the biggest thing this will ever be asked to assemble: 453 labels
@@ -72,10 +89,50 @@ labAddFresh:
CALL numCompare
BRC labAddCrowded ; The arena is smaller than where this name would end.
; The index entry: where the name is about to go, and what it means.
; ---- Room made for it where it belongs ----
;
; The index is kept in order so that looking a name up can halve the table instead of
; walking it, and labFind left LabLo at the place this name sorts to. Everything from there
; on moves up four bytes, from the END backwards so that nothing is written over before it
; has been read.
;
; It costs about a third of the table copied per label added, which against the walk it
; replaces is nothing: the walk was paid once per USE of a label and this is paid once per
; label, and there are several uses of each.
SETD.0 LabWhich
SETD.2 LabCount
CALL numSet
labAddShift:
SETD.0 LabWhich
SETD.2 LabLo
CALL numCompare
BRQ labAddPlace ; Down to where it goes, so there is nothing left to move.
; The one below this place, moved up into it.
CALL labEntryAt
SETD.1 LabEntry
LDD.1.1 ; DP1 is where it goes.
SETD.0 LabWhich
CALL numBack
CALL labEntryAt
SETD.0 LabEntry
LDD.0.0 ; DP0 is where it comes from.
INIB 0d4
labAddShiftByte:
LDA.0
STA.1
INCD.0
INCD.1
DECB
BNB labAddShiftByte
BRI labAddShift
labAddPlace:
SETD.0 LabWhich
SETD.2 LabLo
CALL numSet
CALL labEntryAt
SETD.1 LabEntry
@@ -149,14 +206,39 @@ labAddNo:
labFind:
SETD.2 LabSought
STD.0.2
SETD.0 LabWhich
CALL numZero
labFindLoop:
SETD.0 LabWhich
; ---- Halving the table rather than walking it ----
;
; The names are kept in order, so a lookup is eleven comparisons against two thousand
; entries where a walk was six hundred and seventy. THAT WAS MOST OF THE ASSEMBLER: every
; use of every label paid it, and it grew with the program being built, so assembling
; CosmOS on the machine cost 1.83 billion cycles against the assembler's own 57 million -
; three times the source for thirty two times the time.
;
; LabLo is left at the place the name WOULD go, which is what adding one needs and what a
; walk could never have handed back.
SETD.0 LabLo
CALL numZero
SETD.0 LabHi
SETD.2 LabCount
CALL numSet
labFindStep:
SETD.0 LabLo
SETD.2 LabHi
CALL numCompare
BNC labFindMissing ; Walked the whole table without a match.
BNC labFindMissing ; Nothing left between them, so it is not here.
; The one in the middle. Adding two numbers under two thousand cannot overflow sixteen
; bits, so there is nothing to carry into.
SETD.0 LabWhich
SETD.2 LabLo
CALL numSet
SETD.0 LabWhich
SETD.2 LabHi
CALL numAdd
SETD.0 LabWhich
CALL numHalve
CALL labEntryAt
SETD.1 LabEntry
@@ -169,16 +251,33 @@ labFindLoop:
INCD.0
STB.0
; ---- sameText is already an ordering ----
;
; Q is the difference at the first character that differed, so zero is a match - and the
; Carry Flag from that same subtraction says which way round they were, and survives the
; return because nothing puts the Status register back. A name that runs out while the
; other carries on borrows against the other's character, which sorts the shorter first,
; which is what anybody means by alphabetical.
SETD.1 LabNamePointer
LDD.0.1
SETD.1 LabSought
LDD.1.1
CALL sameText
BRQ labFindGot
BRC labFindAbove ; The one in the table is the smaller, so look above it.
SETD.0 LabWhich
SETD.0 LabHi
SETD.2 LabWhich
CALL numSet
BRI labFindStep
labFindAbove:
SETD.0 LabLo
SETD.2 LabWhich
CALL numSet
SETD.0 LabLo
CALL numStep
BRI labFindLoop
BRI labFindStep
labFindGot:
SETD.1 LabEntry
@@ -285,6 +384,12 @@ LabEntry:
0x00 0x00
LabOffset:
0x00 0x00
; Where a halving search has got to, and where a name that is not there would go. LabLo is
; what labAdd makes room at, which is the whole reason the search hands it back.
LabLo:
0x00 0x00
LabHi:
0x00 0x00
LabWhich:
0x00 0x00
LabLength:
+39
View File
@@ -112,6 +112,45 @@ numCompare:
numCompareDone:
RET
; Takes one off the two byte number at DP0. The mirror of numStep, and wanted for the same
; reason: walking an index backwards is what moving a run of entries up needs.
numBack:
INCD.0
LDA.0
BNA numBackLow ; The low half has something to take, so the high half is safe.
DECD.0
LDA.0
DECA
STA.0
INCD.0
LDA.0
numBackLow:
DECA
STA.0
RET
; The two byte number at DP0 becomes half of itself.
;
; A and B are a CIRCULAR sixteen bit register, so a rotate right brings bit nought back in at
; the top rather than dropping it - which is a halving only once that bit is taken off again.
; The numbers this is asked about are index positions, well under thirty two thousand, so the
; top bit was nought before the rotate and clearing it afterwards loses nothing.
numHalve:
LDA.0
INCD.0
LDB.0
SHR
; The low half goes down FIRST, because taking the wrapped bit off the high half wants B
; for the mask and there is nowhere else to keep it.
STB.0
INIB 0x7F
AND
MVQA
DECD.0
STA.0
RET
; Q is zero if the strings at DP0 and DP1 are the same, both ending in a zero byte.
;
; Down here rather than with the label table, where it started, because four separate