
**********************************************************************
files: Space.TXT, Space.TAP
       FreeMem.ASM, FreeMem.BAS, Longest.BAS, FindVariables.BAS
       Lengths1.BAS, Lengths2.BAS
       MemZero.BAS, MemZero.ASM, MemZero128.ASM, FNmemuse.BAS
**********************************************************************


** calculating free memory
   =======================

At the start of a statement interpretation loop the only item on the machine stack is the error return address, the stack address for that item being held in ERR_SP (23613). The minimum space used for the machine and GOSUB stacks is thus (RAMTOP)-(ERR_SP). The size of the GOSUB stack is that value minus two; it's top is at (ERR_SP)+2.

The ROM's TEST_ROOM routine (called from FREE_MEM) sets aside 80 bytes in its calculation so, making the same allowance, the "Spare" space in the memory map could also be calculated as:
(RAMTOP)-(STKEND)-((RAMTOP)-(ERR_SP)+80) = (ERR_SP)-(STKEND)-80
which gives an answer a few bytes greater than the "PRINT 65536-USR 7962" method suggested in the Logan/O'Hara ROM Disassembly (page 103). The USR 7962 method is simpler, anyway, so it's as good as any other.

A third method is to calculate the difference between SP and (STKEND). The machine SP can't be determined from BASIC except by a USR call, which would change it, so it's a moving target, as is (STKEND); but something like this would give a reasonable value:

FreeMem.ASM [code]
SEED:   equ  23670
mcode:  LD   (SEED),SP          ; 65000  237 115 118 92
        LD   BC,(SEED)          ; 65004  237 75  118 92
        RET                     ; 65008  201
[/code]

So, putting them all together:

FreeMem.BAS [code]
   5 DEF FN p(a)=PEEK a+256*PEEK (a+1)
   6 LET ERRSP=23613: LET STKEND=23653: LET mcode=23296: LET SP=0
  10 PRINT 65536-USR 7962:
     PRINT FN p(ERRSP)-FN p(STKEND)-80:
     LET SP=USR mcode: PRINT SP-FN p(STKEND)-80
[/code]

which gives values for "Spare" of 41247,41271,41251 respectively; plus everything above RAMTOP (=(P_RAMT)-(RAMTOP))can also be considered "Spare", of course. Then the 128k models have at least 96kb available in the extra RAM pages as well, presuming that 2 & 5 are occupied with a BASIC program. In all cases (ADDRESS) = PEEK ADDRESS+256*PEEK (ADDRESS+1). The two options which use a USR call give a value 20-odd bytes smaller because of the USR call itself, which adds several entries to the machine stack.

In practice the ROM's 80 bytes spare minimum requirement isn't practical, as attempting to do much of anything will cause the Spectrum to hang. The minimum spare space after a CLEAR should be the size of all the variables used in the program (after RUN,STOP =(E_LINE)-(VARS)) plus the length of the longest BASIC line (to allow editing) plus 80 bytes.

This routine can be used to find the length of the longest BASIC line.

Longest.BAS [code]
   1 LET max=0: POKE 23619,0:
     FOR a=FN p(23635) TO FN p(23627)-1:
       LET n=256*PEEK a+PEEK (a+1): LET l=FN p(a+2):
       LET l4=l+4 AND n<>1: POKE 23618,1: POKE 23620,9+(l4<=max):
       PRINT n,l4: LET max=max-(max-l4 AND l4>max):
       LET a=a+4+l-1:
     NEXT a: STOP: DEF FN p(a)=PEEK a+256*PEEK (a+1)
[/code]

In the statements "LET l4=l+4 AND n<>1: POKE 23618,1" the '1' should be the line number used for the routine itself. The first stops the line itself being counted; the second sets NEWPPC "line to be jumped to" to itself.


**********************************************************************


** How much space does a number take?
   ==================================

Here's a little program to list the length (excluding the 4-byte header and the trailing ENTER) of each BASIC line in the selected range.

Lengths1.BAS [code]
9990 DEF FN p(a)=256*PEEK ABS a+256*PEEK (ABS a+1)-255*PEEK (ABS a+(a<0))
9991 INPUT l1,l2: FOR p=FN p(23635) TO FN p(23627)-1 STEP 0:
     LET pl=FN p(-p): LET pn=FN p(p+2)
9992 IF pl<l1 THEN PRINT AT 0,0;pl
9993 IF pl>=l1 AND pl<=l2 THEN PRINT p;":";pl,pn-1
9994 IF pl<l2 THEN LET p=p+pn+4: NEXT p
[/code]

To calculate the length of expressions just put them on a line on their own with a PRINT in front and ";" between them so that it passes syntax and then deduct those extra characters from the displayed number; eg:
PRINT CODE "A";VAL "256"
gives 12 - 2 = 10; whereas:
PRINT CODE "A";256
gives 15 - 2 = 13, showing that the first version saves 3 bytes.


**********************************************************************


** Making Room
   ===========

As long as the screen isn't used there's 7kb in the display & attributes area which can be used for variables, thus giving 7kb more for BASIC. That's the only spare RAM available on the 48k model without adding extra hardware. Variable names are restricted to single-letter as the technique involves defining a DEF FN variable list starting at 16384.

The conventional space can be used more efficiently by changing how code is written and data stored, primarily by not using methods which employ floating point numbers. The simplest method to reduce the size of the BASIC is to define literals as constants. Using NOT PI for zero, plus the other old favourites, may be compact but don't make for readable code. Define 0,1,2 and any other commonly used numeric literals as k0,k1,k2,etc or somesuch; this will save several hundred bytes in any large program. Use string arrays to hold small numbers rather than number arrays. Keep variable names as short as possible consistent with readability.

What is gained in space will be lost in time, so once a program's got substantially smaller and noticeably slower then consideration will have to be given to how to speed it up again. One simple rule is to put all code addressed by GO SUBs or GO TOs, and all lines containing DEF FN or DATA statements, as near to the front as possible. Pre-define the most commonly used variables first so that they are at the start of VARS.

Avoid doing RESTORE repeatedly. Once a program is running the location of DATA lines isn't going to change, so if the program repeats RESTOREs then just do them once in a loop at the start and save the values of DATADD in an array, then just POKE the relevant address from the array into DATADD each time instead of doing a RESTORE. RESTORE will be quicker if the lines are near the start, but the further it has to search the longer it takes, whereas two POKEs always take the same time, and the program only needs to be a few kb in size before the POKEs become quicker than the RESTORE.

   ===========

Variables of the type "complete simple strings" (ie. declared as a$, b$, etc.) where the space allocated to the variable may change; after a LET the new value is always reassigned to a new copy of the variable and the old space reclaimed, even if the size hasn't changed. All other types of variables have a fixed size and aren't moved when assigned a new value. They'll only be moved indirectly if a simple string space has been reclaimed lower down, when everything above the old space gets moved down.

If a DIM command refers to an existing array (eg. to erase its contents or change its dimensions) then that will also be reassigned and reclaimed.

Here's a little BASIC routine which locates variables; just GOSUB 3000 and follow the prompts. As it prints each variable name until it finds the requested one, to get it to list all of the currently declared variables just enter a name which doesn't exist.

FindVariables.BAS [code]
3000 RESTORE 3000: DIM v$(7,20):
     FOR a=1 TO 7: READ v$(a): NEXT a:
     DATA "","string","number (short name)","number array",
          "number (long name)","string array","FOR...NEXT control"
3001 CLS : PRINT INVERSE 1;"SEARCH FOR VARIABLE"; INVERSE 0;": of Type:"':
     FOR a=2 TO 7: PRINT a;" = ";v$(a): NEXT a:
     INPUT "Type ";o,"Name ";o$: LET o=FN i(o,2,7): GO TO FN c()+(o>0)
3002 LET cd=o*32+CODE o$-96: LET f=0:
     FOR i=FN p(23627) TO FN p(23641)-2: PRINT " ";: LET d=PEEK i:
       IF d=cd THEN LET f=i
3003 LET e=INT (d/32): LET l$=CHR$ (d-e*32+96): PRINT l$;
3004 IF e=2 OR e=4 OR e=6 THEN LET i=i+2+FN p(i+1):
     PRINT "$" AND e<>4;"()" AND e<>2;
3005 IF e=3 THEN LET i=i+5
3006 IF e=7 THEN LET i=i+18: PRINT "...";
3007 IF e=5 THEN LET f=0:
     FOR a=i+1 TO FN p(23641)-2: IF PEEK a<128 THEN
       LET u=PEEK a: LET l$=l$+CHR$ u: PRINT CHR$ u;: NEXT a
3008 IF e=5 THEN LET i=a+5: LET u=PEEK a-128: LET l$=l$+CHR$ u: PRINT CHR$ u;:
       IF l$=o$ THEN LET f=i-4-LEN o$
3009 IF f THEN PRINT '"found at ";f: RETURN
3010 NEXT i: PRINT '"NOT FOUND": RETURN
3099 DEF FN c()=FN p(23621):
     DEF FN i(n,a,z)=INT ABS n AND n>=a AND n<=z:
     DEF FN p(a)=PEEK a+256*PEEK (a+1)
[/code]

   ===========

A few other space saving ideas (all with a time penalty):
* Use of logical operators and/or DEF FNs can make code more compact but perhaps less easy to understand (see FN c() and FN i() above).
* Look for repetition in the code. A group of lines which occur more than once might be combined as a single subroutine. A series of similar lines might be compressed into a FOR...NEXT loop with an array.
* If you have a large multi-D string array with lots of empty space padding then consider using a compacted 1-D array with an index.

   ===========

This line can be MERGEd and used to check the size of a selected line or range of lines, which will help with deciding which parts might need altering. It also shows how using logical operators and DEF FNs can both compact the code and make it more difficult to understand - and, incidentally, how to write a conditional loop without using IF, GOTO or FOR...NEXT (the inner printing loop) or without an explicit repeat or exit point (the outer input loop) - not necessarily recommended as standard procedure. The statements in [color=green]green[/color] are those addressed by the POKEs.

Lengths2.BAS [code]
9999 INPUT a,z:
     LET a=FN i(a,1,9999):
     LET z=FN i(z,a,9999):
     LET p=FN p(23635,+1):
     LET e=FN p(23641,+1):
     LET t=0:
[color=green]       LET n=FN p(p+1,-1):[/color]
       LET l=FN p(p+2,+1)+4:
       POKE 23620,10+(2 AND NOT FN i(n,a,z)):
[color=green]         PRINT n,l:[/color]
         LET t=t+l:
[color=green]       LET p=p+l:[/color]
       POKE 23620,14-(7 AND n<z AND p<e):
[color=green]     PRINT ,t':[/color]
     GO TO FN c()+NOT (a+z):
     DEF FN c()=FN p(23621,+1):
     DEF FN i(n,a,z)=INT ABS n AND n>=a AND n<=z:
     DEF FN p(a,s)=PEEK a+256*PEEK (a+s)
[/code]

   ===========

I find Fuse the easiest emulator for capturing printer output. Just select "Emulate printers" in Options:Peripherals and LPRINT/LLIST will go to a text file in the current directory. Z80 can also print to a text file when If.1 is enabled. Some other emulators; eg. BASin, ZXSpin, EightyOne; capture printer output to a BMP graphic file instead of a text file.

As pointed out, transferring the program to a Spectrum 128k is simple enough. However, making use of the extra memory from BASIC is not so straightforward and would entail substantial restructuring of the program. All BASIC can do with the extra pages is SAVE! & LOAD! the RAMdisk, or reference the RAM directly via PEEK & POKE or indirectly via LET. Also, if the top page is going to be swapped then it becomes unavailable for the main BASIC program unless you're very careful as to when routines are called which are in the top page.

More of a problem are the VARS, ELINE, WORKSP & STKBOT areas, which would extend into the top page before the actual BASIC code ever did. If STKEND is higher than 48kb when the program is running then you're not going to be able to access any of the extra RAM pages from BASIC without crashing it, except indirectly via the RAMdisk.

Large BASIC programs will run OK; eg. one of mine which I've just checked has STKEND at 57064 and RAMTOP at 65367; but it restricts what can be done with the extra memory from BASIC, apart from saving extra BASIC, CODE & DATA files into the RAMdisk pages and loading them as required. Of course, whenever something is loaded into main memory it overwrites what was there before, so whatever that was will also need to be saved into the RAMdisk as well if it's going to be needed again. So program & data need to have a much more structured & modular design than might otherwise be the case.

Initially it would probably be easier to look at the code & data to see if any significant memory savings could be made within it; eg. using named rather than explicit literals; using string instead of number arrays; using short variable names; removing REMs. For example, with a program I worked on recently, which is only about 6k, I saved 500 bytes just by naming all the common numeric literals and using the names instead of the numbers; eg:

[code]
   1 LET k0=NOT PI: LET k1=SGN PI: LET k2=k1+k1: LET k3=INT PI:
     LET k4=k3+k1: LET k5=k4+k1: LET k6=k5+k1: LET k7=k6+k1: LET k8=k7+k1:
     LET kB=k8+k3: LET kF=k5*k3: LET kG=k4*k4: LET k32=kG*k2: LET k7F=127:
     LET kFF=255: LET kFG=kFF+k1: LET kM=1000: LET k5M=k5*kM:
     LET k16k=16384: LET k32k=k16k*k2: LET k48k=k16k*k3: LET k64k=k16k*k4:
     LET BANKM=23388: LET DOS=31000
[/code]


**********************************************************************


** Unused RAM check
   ================

If I'm looking for a chunk of free memory I just switch to the debugger's Memory tab and drag down the scroll-bar continuously and visually check for a bunch of zeroes whizzing past, or if I'm looking at a file externally I'll use the HxD hex editor and do the same thing.

Otherwise, here's a routine which lists addresses for areas of 175 consecutive zeroes between (STKEND) and 65535. To check for some other value just change the value for A in the LOOP line. The program waits for a key when the screen is full. If you're using the extended RAM on a 128k model then it will need changing to switch in each RAMpage in turn.

[center][img]http://www.users.globalnet.co.uk/~jg27paw4/pourri/memzero.gif[/img][/center]

MemZero.BAS [code]
   2 DEF FN p(a)=PEEK a+256*PEEK (a+1)

1000 PLOT 40,0: FOR a=FN p(23653) TO 65535
1002 IF NOT PEEK a THEN PLOT PEEK 23677,PEEK 23678+1:
       IF PEEK 23678=175 THEN GO SUB 1010: PRINT a-175:
         IF PEEK 23689=3 THEN FOR r=21 TO 0 STEP -1: PRINT AT r,0,,AT r,0;: NEXT r:
           PLOT PEEK 23677,0
1003 IF PEEK a THEN POKE 23678,0: IF NOT PEEK (a-1) THEN GO SUB 1010
1004 NEXT a: STOP

1010 POKE 23677,40+((PEEK 23677+1-40) AND PEEK 23677<255):
     PLOT PEEK 23677,0: RETURN
[/code]

MemZero.ASM [code]
MEMZERO:ld   a,2
        call $1601      ;CHAN_OPEN
        call $0d6b      ;CLS
        ld   a,2
        call $1601      ;CHAN_OPEN
        ld   bc,$0028   ;x=40, y=0
        call $22e5      ;PLOT_SUB: PLOT 40,0
        ld   hl,(23653) ;HL=(STKEND)
LOOP:   xor  a
        cp   (hl)
        jr   nz,NOTZERO

        push hl         ;found a zero
        ld   bc,(23677) ;COORDS
        inc  b          ;B =y+1
        call $22e5      ;PLOT_SUB: PLOT x,y+1
        ld   a,(23678)  ;COORDS_y
        cp   175
        jr   nz,NEXT

        call INC_X      ;found 175 zeroes
        pop  hl
        push hl
        and  a
        ld   de,175
        sbc  hl,de
        ld   b,h
        ld   c,l
        call $2d2b      ;STACK_BC
        call $2de3      ;PRINT_FP
        ld   a,$0d
        rst  $10        ;newline
        ld   a,(23689)  ;S_POSN_line
        cp   3
        jr   nz,NEXT

        ld   bc,$0000   ;address column is full
        call $1f3d      ;PAUSE_1
        ld   b,24
        call $0e44      ;CL_LINE
        ld   a,$16
        rst  $10
        xor  a
        rst  $10
        xor  a
        rst  $10
        ld   a,(23677)  ;COORDS_x
        call NEW_X
        jr   NEXT

NOTZERO:xor  a          ;found something other than zero
        ld   (23678),a  ;COORDS_y
        push hl
        dec  hl
        cp   (hl)
        call z,INC_X

NEXT:   pop  hl
        inc  hl
        ld   a,h
        or   l
        jr   nz,LOOP

        ret

INC_X:  ld   a,(23677)  ;COORDS_x
        inc  a          ;increment the x PLOT column
        jr   nz,NEW_X   ;at right margin?
        ld   a,40       ;reset x to left margin @ 40
NEW_X:  ld   (23677),a
        ld   c,a
        ld   b,0
        call $22e5      ;PLOT_SUB: PLOT x+1,0
        ret
[/code]

   ======================

I've changed the above listing to handle 128k models as well and added a bit of colour; the plot colour changes each time the display wraps around. POKE 23681 with the search value. Press a key to continue when the address column fills up. Also, instead of just printing the addresses of 175 byte fragments it now prints the address of each matching block followed by its full size, so there's no adding up required. Note that for the 128k pages 0-7 are loaded in turn and only the top 16k scanned, so addresses will be no lower than 49152 ($c000).

Run with: POKE 23681,[value]: RANDOMIZE USR 31000: POKE 23624,PEEK 23693
The POKE POKEs BORDCR with ATTR_P to restore the edit line colours.

MemZero128.ASM [code]
        org  31000
MemZero128:
        LD   A,$02
        CALL $1601           ;CHAN_OPEN
        CALL clear
        LD   BC,$0038
        CALL $22E5           ;PLOT_SUB x=56,y=0
        LD   HL,$0000
        LD   ($5C7F),HL      ;(23679)=start of matching block
        LD   A,$38           ;PAPER white, INK black
        LD   ($5C8D),A       ;(ATTR_P)
        LD   E,$00
        LD   HL,$4000        ;HL=16384 start point for 48k
        BIT  4,(IY+$01)
        JR   Z,Select        ;128k?, jump if not

        LD E,$07
PageLoop:
        LD   HL,$BFFF
        LD   (HL),$3E        ;place RAMTOP marker at $C000-1
        INC  HL              ;HL=49152 start point for 128k
        DI
        LD   A,($5B5C)       ;A =(BANKM)
        AND  $F8
        OR   E
        LD   ($5B5C),A       ;A =(BANKM)
        LD   BC,$7FFD
        OUT  (C),A           ;switch to page 7
        EI

Select: LD   A,E
        LD   ($5C48),A       ;(BORDCR)
ByteLoop:
        LD   A,($5C81)       ;A =(23681) check byte
        CP   (HL)            ;check for match on current byte
        JR   NZ,NoMatch
        PUSH HL
        DEC  HL
        LD   A,($5C81)       ;A =(23681) check byte
        CP   (HL)            ;check for match on previous byte
        JR   Z,UpdateY
        INC  HL
        LD   ($5C7F),HL      ;(23679)=start of matching block
UpdateY:LD   BC,($5C7D)      ;BC=(COORDS)
        INC  B
        CALL $22E5           ;PLOT_SUB
        LD   A,($5C7E)       ;A =(COORDS)_y
        CP   $AF
        CALL Z,UpdateX
        JR   TestRAM

NoMatch:XOR  A
        LD   ($5C7E),A       ;reset (COORDS)_y
        PUSH HL
        CALL CheckSize
TestRAM:POP  HL
        INC  HL              ;check for end of RAM
        LD   A,H
        OR   L
        JR   NZ,ByteLoop
        CALL CheckSize
        LD   A,($5C48)       ;A =(BORDCR)
        DEC  A
        RET  M
        LD   E,A             ;E =RAMpage#
        JR   PageLoop        ;back to check next page

clear:  LD   B,$18
        CALL $0E44           ;CL_LINE clear 24 lines
        LD   A,$16
        RST  $10             ;PRINT AT 0,0;
        XOR  A
        RST  $10
        XOR  A
        RST  $10
        RET

CheckSize:
        LD   BC,($5C7F)     ;BC=(23679)=start of matching block
        LD   A,B
        OR   C
        RET  Z
        PUSH HL
        PUSH BC
        CALL UpdateX
        POP  BC
        POP  HL
        AND  A
        SBC  HL,BC
        LD   DE,$00AF        ;DE=175
        SBC  HL,DE
        JR   C,Small
        ADD  HL,DE
        PUSH HL
        CALL PrintPg
        POP  BC
        CALL PrintSz
Small:  LD   HL,$0000
        LD   ($5C7F),HL      ;(23679)=start of matching block
        RET

PrintPg:CALL $2D2B           ;STACK_BC
        LD   A,($5C48)       ;A =(BORDCR)
        ADD  A,$30
        RST  $10             ;RAMpage#
        LD   A,$3A
        RST  $10             ;":"
        JR   PrintFP

PrintSz:CALL $2D2B           ;STACK_BC
PrintFP:CALL $2DE3           ;PRINT_FP
        LD   A,$0D
        RST  $10             ;ENTER
        LD   A,($5C89)       ;A =(S_POSN)_line
        CP   $02
        RET  NZ
        LD   BC,$0000        ;BC=0 for PAUSE 0
        CALL $1F3D           ;PAUSE_1
        CALL clear
        LD   A,($5C7D)       ;A =(COORDS)_x
        CALL NotEnd          ;PLOT x,0
        RET

UpdateX:LD   A,($5C7D)       ;A =(COORDS)_x
        INC  A
        JR   NZ,NotEnd
        LD   A,($5C8D)       ;A =(ATTR_P)
        INC  A               ;increment INK
        AND  $3B             ;restrict INK to 0,1,2,3
        LD   ($5C8D),A       ;(ATTR_P)
        LD   ($5C8F),A       ;(ATTR_T)
        LD   A,$38
NotEnd: LD   ($5C7D),A       ;(COORDS)_x
        LD   C,A
        LD   B,$00
        CALL $22E5           ;PLOT_SUB
        RET
[/code]


**********************************************************************


** calculating FN memory usage
   ===========================

Here's a way to get both the calculator & machine stacks included when you're calculating function memory usage in BASIC. Just put the DEF FN in 1001 and the FN call in line 1030.

[center][img]http://www.users.globalnet.co.uk/~jg27paw4/pourri/fnmemuse.gif[/img][/center]

FNmemuse.BAS [code]
1001 DEF FN t$(n,t,d)
     =STR$ INT (n/t)+(":" AND d=6 AND t>500)
     +VAL$ (("FN t$("
            +STR$ (n-INT (n/t)*t)+","
            +STR$ (t/d)+","
            +STR$ (d-4+(8 AND d=6))
            +")" AND t>=500)
           +("""""" AND t<500))
1002 DEF FN a(a$)=0: REM must be 1st statement in line
1003 DEF FN p(a)=PEEK a+256*PEEK (a+1)
1004 CLEAR 65535: DIM b$(500): REM put stack out of the way for a bit
1006 RESTORE 1002: LET da=FN p(23639)+8: REM point to a$ pointer
1008 POKE da+3,0:
     RANDOMIZE 65000-500: POKE da+4,PEEK 23670:  POKE da+5,PEEK 23671:
     RANDOMIZE 500:       POKE da+6,PEEK 23670:  POKE da+7,PEEK 23671:
     RANDOMIZE da:        POKE 23563,PEEK 23670: POKE 23564,PEEK 23671:
     LET a$=b$: POKE 23564,0: REM load new stack space with 500 spaces
1010 CLEAR 65000: REM move SP to use new stack space
1015 GO SUB 2050: LET sp1=space
1020 LET free1=65536-USR 7962
1030 PRINT AT 0,0;FN t$(2264800,1800000,10);
           AT 1,0;65536-USR 7962,,: LET free2=0
1040 FOR c=0 TO 31:
       LET c$=SCREEN$(1,c):
       IF c$<>" " THEN LET free2=free2*10+VAL c$: NEXT c
1050 GO SUB 2050: LET sp2=space
1060 PRINT "FN mem.use "'
           "= calc: ";free1-free2;" + mach: ";sp1-sp2;
           " = ";free1-free2+sp1-sp2:
     STOP
2049 REM find "high water mark" of machine stack
2050 PRINT FLASH 1;">>>";CHR$ 8;CHR$ 8;CHR$ 8;
2051 FOR a=64500 TO 65000 STEP 2:
       IF FN p(a)=8224 THEN NEXT a: REM = 32+256*32; ie. 2 spaces
2052 LET space=a-64500: RETURN
[/code]


**********************************************************************


