Read the next block while the program is busy with this one

A file is read front to back, so when something asks for a block, the one after
it is almost certainly wanted next. sbfsReadOne asks the disk for it straight
away and hands back the block that was wanted - so the transfer happens while
the caller is busy, and the waiting is mostly over by the time it comes back.

Printing a fourteen kilobyte file: 1,064,498 cycles at two thousand a block
becomes 976,882, and 1,576,562 at ten thousand becomes 1,032,889. The second
figure barely moves between those - seven per cent from an instant disk to a
slow one, where without it the same change costs sixty eight. A machine that
reads ahead stops caring very much how fast its disk is.

NOT FOR DIRECTORY SEARCHES, and that was tried first and thrown away. A scan
stops the moment it matches, so the block it would read ahead is one nobody
will ever look at - a transfer to fetch and another wait to throw away. It was
nineteen per cent SLOWER on a lookup at ten thousand cycles a block. Reading
ahead is a bet that the next block is wanted, and a search is exactly the case
that hopes it is not. The scan loop is untouched.

Three per cent is what it costs when the disk is instant, which is the
bookkeeping with nothing to hide behind it, and the default.

The disk has one buffer, so the only way to know what is in it is to remember
what was last asked for. Every read records that; a write clears it, because a
write fills the buffer from memory and no read asked for what is in it. Getting
that wrong would blit whatever happened to be there and call it the block
somebody wanted.

The read ahead is bounded by the file's own length, so reading the last block
does not fetch whatever follows the file on the disk - which belongs to somebody
else and would be paid for twice, once to fetch and once to discard.

It cost one bug, and an instructive one: the next index was worked out by
stepping SbfsIndex, which is the CALLER'S and is still wanted after the return -
handleFileBlock compares it against the file's length to see whether this was the
short last block. Every block reported the wrong number of bytes, and the output
of Type skipped five lines in the middle. It has a place of its own now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
Anachronaut
2026-08-25 22:36:22 -04:00
co-authored by Claude Opus 5
parent d4cba36c5e
commit db3d349da8
2 changed files with 123 additions and 4 deletions
+29
View File
@@ -701,6 +701,35 @@ end of the file - files are contiguous, so block nine of a three block file is a
belonging to something else, and writing it would put one file's bytes inside another with
nothing anywhere saying so.
### Reading Ahead:
A file is read front to back, so when a program asks for a block, the one after it is
almost certainly wanted next. `sbfsReadOne` asks the disk for it straight away and hands
the caller the block it wanted - so the transfer happens while the program is busy with
what it already has, and the wait is mostly gone by the time it comes back.
Nothing is done differently and nothing is done out of order. The machine simply stops
standing still.
**It is not done for directory searches, and that is not an oversight.** A scan stops the
moment it matches, so the next block is one nobody will ever look at: it costs a transfer to
fetch and another wait to throw away. Tried there, it was nineteen per cent *slower*. Read
ahead is a bet that the next block is wanted, and a search is exactly the case that hopes it
is not.
What it is worth, printing a fourteen kilobyte file:
| Cycles a block | Without | With |
| -- | -- | -- |
| 0 | 936,626 | 962,959 |
| 2,000 | 1,064,498 | 976,882 |
| 10,000 | 1,576,562 | 1,032,889 |
The second column barely moves. From an instant disk to a slow one the cost rises seven per
cent, where without it the same change costs sixty eight - which is the point: **a machine
that reads ahead stops caring very much how fast its disk is.** The three per cent it costs
at zero is the bookkeeping, paid when there is nothing to hide behind it.
### Saving Something Twice:
Which is why saving a document is not the same as writing a file, and why sbfsSaveFile exists rather than each tool doing it. A file that has grown will usually not fit where it was, so saving it means putting it somewhere else and letting go of where it was - and **the obvious order is a trap**:
+94 -4
View File
@@ -1224,13 +1224,63 @@ sbfsReadOne:
SETD.2 SbfsIndex
CALL sbfsAddWord
; ---- READ AHEAD ----
;
; Is this block already coming? It will be if the last time through asked for the one
; before it, which is what reading a file front to back does every time. Then there is
; nothing to start: the disk has been fetching it while the caller was busy with the last
; one, and all that is left is to wait for it.
;
; This is the path a search is NOT. A directory scan stops the moment it matches, so
; asking for the next block there is asking for one nobody will look at - and it was
; nineteen per cent slower for exactly that reason. A file is read to its end, so every
; block asked for early is one that would have been asked for anyway.
SETD.0 SbfsBufferKnown
LDA.0
BRA sbfsReadOneFresh
SETD.0 SbfsBufferBlock
SETD.2 SbfsBlock
CALL sbfsCompareWord
BNQ sbfsReadOneFresh
RCAL sbfsWaitDisk ; Already on its way, so only the waiting is left.
INIB 0x02
AND
BNQ sbfsReadOneDone
BRI sbfsReadOneHere
sbfsReadOneFresh:
CALL sbfsReadBlock
BNQ sbfsReadOneDone ; The read failed, and Q says so.
sbfsReadOneHere:
PSHD.3
POPD.1
CALL sbfsBufferOut
; And the one after it, started now, if the file has one. Bounded by the file's own
; length so that reading the last block does not fetch whatever follows the file on the
; disk - which belongs to somebody else and would be paid for twice: once to fetch and
; once to throw away.
; IN A PLACE OF ITS OWN, not in SbfsIndex. What block was asked for is the caller's, and
; it is still wanted after this returns - handleFileBlock compares it against the file's
; length to work out whether this was the short last block. Stepping it here made every
; block report the wrong number of bytes.
CALL sbfsFileExtent
SETD.0 SbfsAheadIndex
SETD.2 SbfsIndex
CALL sbfsSetWord
SETD.0 SbfsAheadIndex
CALL sbfsStepWord
SETD.0 SbfsAheadIndex
SETD.2 SbfsWantBlocks
CALL sbfsCompareWord
BNC sbfsReadOneLast ; The next one is past the end of the file.
SETD.0 SbfsBlock
CALL sbfsStepWord
RCAL sbfsStartRead
sbfsReadOneLast:
RSTA
RSTB
CCF
@@ -1719,6 +1769,8 @@ sbfsBufferIn:
; Puts the disk's buffer down as the block named by SbfsBlock. Q is zero if it worked.
sbfsWriteBlock:
RCAL sbfsWaitDisk ; Anything still going finishes before this asks for more.
RCAL sbfsForgetBuffer
SETD.0 SbfsBlock
LDA.0
OUTA 0x20
@@ -1734,6 +1786,32 @@ sbfsWriteBlock:
; Reads the block named by SbfsBlock into the disk's buffer. Q is zero if it worked.
sbfsReadBlock:
RCAL sbfsStartRead
RCAL sbfsWaitDisk
INIB 0x02
AND ; Q is the error bit, so zero means it worked.
RET
; ---- Asking for a block without waiting for it ----
;
; The half of a read that costs nothing, and the whole of what makes reading ahead
; possible. Whatever the disk was still doing is collected first, because a controller
; given a command while it is busy has no good answer - so this is the one place that
; guarantees it is not, and every read goes through it.
;
; WHICH BLOCK IS COMING IS WRITTEN DOWN, because the disk has one buffer and the only way
; to know what is in it is to remember what was last asked for. Anything that fills the
; buffer some other way must say so by clearing it, or a later read would blit whatever
; happened to be there and call it the block it wanted.
sbfsStartRead:
RCAL sbfsWaitDisk
SETD.0 SbfsBlock
SETD.1 SbfsBufferBlock
CALL sbfsCopyWord
INIA 0x01
SETD.0 SbfsBufferKnown
STA.0
SETD.0 SbfsBlock
LDA.0
OUTA 0x20
@@ -1742,10 +1820,14 @@ sbfsReadBlock:
OUTA 0x21
INIA 0x01
OUTA 0x22
RCAL sbfsWaitDisk
INIB 0x02
AND ; Q is the error bit, so zero means it worked.
RET
RRET
; The buffer is about to hold something this cannot describe.
sbfsForgetBuffer:
RSTA
SETD.0 SbfsBufferKnown
STA.0
RRET
; ---- Waiting for the disk ----
;
@@ -2877,6 +2959,14 @@ SbfsAt:
0x00 0x00
SbfsScanIndex:
0x00 0x00
; ---- What the disk's one buffer holds, or is about to ----
SbfsBufferBlock:
0x00 0x00
SbfsBufferKnown:
0x00
SbfsAheadIndex:
0x00 0x00
SbfsTarget:
0x00 0x00
SbfsUpParent: