Speeding up programs by hacking the SysVars

Positively pleased to properly discover this area of the forum! I have a couple of recent tricks that might be of interest - they're probably old hat to most of you but on the off-chance, I'll post links to them here.

First up, you can temporarily hack the value of PROG to be the value of NXTLIN, and from there on you can execute loops (for instance), much quicker - the interpreter isn't going to step through all your program code to find the line it needs to branch to, it's just going to start looking from the address that you've poked in PROG. As long as you remember to save the original address, and put it back again later, you can use this trick to make decent savings on loops, as shown on the blog;

Hacking PROG with NXTLIN to speed up BASIC loops

Comments

  • Something along those lines would work with any command which does a PROG search; eg.:
    FN, NEXT, GO TO, GO SUB, MERGE, RESTORE, RETURN, RUN
    Similarly with VARS, if variables are pre-declared so that their positions can be controlled, then VARS could be shifted to point to the lowest addressed variable in the current routine.
    In fact, if VARS is shifted before the variables used by a routine are declared, then it would be possible to have separate local & global variables with the same name. Something similar can be done by defining FNs which are just place holders for variables, but in that case they're restricted to single-letter names.

    As regards your other question, multi-statement lines save space, not time. Each statement is interpreted in isolation, so the only difference is whether it ends with a colon or ENTER. Checking 1BD1 NEXT-LINE and 1BEE CHECK-END indicates that it does slightly more work if there is more than one statment on a line, as colon is checked after ENTER, and a CALL 198B,EACH-STMT is required for each subsequent statement.
  • Something along those lines would work with any command which does a PROG search;

    Good point!

    As regards your other question, multi-statement lines save space, not time. Each statement is interpreted in isolation, so the only difference is whether it ends with a colon or ENTER. Checking 1BD1 NEXT-LINE and 1BEE CHECK-END indicates that it does slightly more work if there is more than one statment on a line, as colon is checked after ENTER, and a CALL 198B,EACH-STMT is required for each subsequent statement.

    Cool. I don't know why, I just sort-of assumed that an obvious optimisation the interpreter would make would be to know that the looping it was doing didn't need to involve a new search through from PROG for the start of the loop again, if it knew that the entire loop had been contained in a sole multi-statement line. But you're right, not only is the multi-statement approach NOT faster, it's likely slower because of the overhead of the next statement in a line processing. I might time that a bit just to put the idea in my head to bed for good :-)

  • This is a fantastic trick - well done Robsoft.

    BB - can you expand/give an example of how you would have multiple copies of the same variable name?

    Many thanks

    Paddy
  • Cool blog! I hope there will be more posts soon :)

    Thanks for the work Robsoft
  • BB - can you expand/give an example of how you would have multiple copies of the same variable name?

    The way I've done it before now is to use the parameter space allocated by a DEF FN as a mini-variables area, which the ROM always searches before the VARS area. Taking a bit out of my (as yet unfinished) version of Akalabeth:
       7 DEF FN z(r,c,z$)=8101:
         LET da=VAL "23563": LET dt=VAL "23639": LET seed=VAL "23670":
         RESTORE 7: RANDOMIZE FN p(dt)+8: LET zlo=PEEK seed: LET zhi=PEEK (seed+1)
    
    This locates the parameter space for the FN and saves the start address in zlo+zhi. (The FN doesn't have to be the first statement on a line, but it's quicker & easier to find it if it is. The RANDOMIZE would be +9 if it was a string function, to skip over the '$' in the name. The SysVars referenced are DEFADD, DATADD, SEED.)

    In this case, I'm referencing the FN like this:
    GO SUB FN z(row_expression, col_expression, string_expression)
    

    and the sub-routine at 8101 accessed via FN z() prints the text in a window, checking for wordwrap, when to scroll and so on. To enable the sub-routine to reference the function parameters, tell the ROM where to look:
    8101 POKE da,zlo: POKE da+1,zhi
    
    and to preserve them when finished, disable the reference (the ROM only checks the hi-byte):
    8110 POKE da+1,0: RETURN
    

    So there can be global variables r,c,z$ present as well as those named in the function, and other functions could use those names as well, but they all would be held separately and wouldn't interfere with each other. Values can be returned from the function via the parameter space as well, but that's another story ...

    From the earlier post it occurred to me that a similar thing could be done by shifting the VARS pointer. As long as all variables for each area are declared during initialisation, so that they won't need to be created once the main program is running, then they could be maintained independently and share common code & names without interfering with each other.

    As I only thought of this yesterday, there may be as yet unforeseen complications with the idea. The one which immediately comes to mind is simple strings, which are always re-allocated new space below E_LINE when a value is assigned, even if it's the same length as the old value. This can be stopped by assigning all strings with DIM, but that might not be convenient. Having VARS discontiguous from the end of PROG breaks at least one emulator as well.. (I have some programs which hold m/code in between, which crash in BASin.)
  • edited September 2016
    Hi Chaps,

    Did some testing with Robsoft's idea of setting PROG to NXTLIN. The test program simply fills the screen with a "L", does NOT use the PROG/NXTLIN trick and has lower case variables.

    10 CLS 
    20 LET c=0: LET r=0: LET y=PEEK 23635: LET z=PEEK 23636
    30 REM 
    40 REM Set PROG to NXTLIN
    50 REM 
    60 REM POKE 23635,PEEK 23637: POKE 23636,PEEK 23638
    70 FOR r=0 TO 21
    80 FOR c=0 TO 31
    90 PRINT AT r,c;"L"
    100 NEXT c
    110 NEXT r
    120 REM 
    130 REM Reset PROG
    140 REM 
    150 POKE 23635,y: POKE 23636,z
    160 STOP
    

    According to BASin's Profiler this took 7.4 seconds (368.7 frames) to excute lines 70 to 110.

    The next step was to implement Robsoft's trick i.e. removing the REM from line 60. With this done the execution time for lines 70 to 110 reduced to 7.0 seconds (352.2 frames) or ~4.5%.

    The next test converted the variable names to uppercase as follows:

    10 CLS 
    20 LET C=0: LET R=0: LET Y=PEEK 23635: LET Z=PEEK 23636
    30 REM 
    40 REM Set PROG to NXTLIN
    50 REM 
    60 POKE 23635,PEEK 23637: POKE 23636,PEEK 23638
    70 FOR R=0 TO 21
    80 FOR C=0 TO 31
    90 PRINT AT R,C;"L"
    100 NEXT C
    110 NEXT R
    120 REM 
    130 REM Reset PROG
    140 REM 
    150 POKE 23635,Y: POKE 23636,Z
    160 STOP
    

    Overall time in seconds remaind at 7.0 but the frame count reduced to 350.8. This means the total reduction ~4.9%.

    My final test was to take the above code and place the FOR...NEXT loops all on one line:

    10 CLS 
    20 LET C=0: LET R=0: LET Y=PEEK 23635: LET Z=PEEK 23636
    30 REM 
    40 REM Set PROG to NXTLIN
    50 REM 
    60 POKE 23635,PEEK 23637: POKE 23636,PEEK 23638
    70 FOR R=0 TO 21: FOR C=0 TO 31: PRINT AT R,C;"L": NEXT C: NEXT R
    80 REM 
    90 REM Reset PROG
    100 REM 
    110 POKE 23635,Y: POKE 23636,Z
    120 STOP
    

    Unfortunately, this slowed the code down and the Profiler results were 7.1 seconds (354.4 frames).

    So in summary there is up to a ~5% speed improvement to be had by predefining your variables in uppercase and setting PROG to NXTLIN for loops.

    A great tip for when maximum speed is needed.

    Paddy

    Post edited by Paddy Coleman on
Sign In or Register to comment.