Thursday, February 22, 2018

Speed up regular Microcolor BASIC on the MC-10

No ROM replacement needed for this... but it's not a lot faster.
It uses the same patch to the CHRGET function on the direct page used in the the latest ROM.
The patch will be embedded in the REM in line 0 after the first run.
Then delete lines 9998-10000 and move the EXEC to line 1 in place of the GOSUB and save it.
It will already be embedded in the REM and there's no need to read/poke the data again.
The patch is actually resident in RAM on the direct page until you turn off the machine or it crashes.


BASIC code:

0 REM012345678901234567890123456789012345
1 GOSUB 10000
9998 DATA 60,54,55,206,67,101,236,1,221,246,236,3,221,248,236,5,221,250,236,7,221,252
9999 DATA 51,50,56,57,240,129,58,37,1,57,126,225,204
10000 FORI=0TO34:READ A:POKE 17227+I,A:NEXT:EXEC17227:RETURN


And the assembly for the TASM cross assembler:

; Simple speed up for MC-10 Microcolor BASIC
; (C)2018  James Diffendaffer
; May be freely redistributed

.MSFIRST        ; Most Significant byte first

#define EQU     .EQU
#define ORG     .ORG
#define RMB     .BLOCK
#define FCB     .BYTE
#define FCC     .TEXT
#define FDB     .WORD
#define END .END
#define FCS .TEXT

#define equ     .EQU
#define org     .ORG
#define rmb     .BLOCK
#define fcb     .BYTE
#define fcc     .TEXT
#define fdb     .WORD
#define end .END
#define fcs .TEXT

org $434B

pshx
psha
pshb
ldx #PATCH
ldd 1,X
std $F6
ldd 3,x
std $F8
ldd 5,x
std $FA
ldd 7,x
std $FC
pulb
pula
pulx

rts

PATCH
FCB $F0
org $00F6

cmpa      #':' ; set Z flag if statement separator
bcs       AA ; perform more tests if not
rts ; return if >= ':' 
AA jmp $E1CC ; jump to the parser back end

end

Wednesday, February 7, 2018

Finalizing a new ROM release

It's time for a new MC-10 ROM release.  I've squeezed about as much into 8K as possible within a reasonable amount of time.  As of now there are around 10 bytes free in the ROM, but it's not enough to implement storing the pointer to the next line... which was the only other optimization I thought might fit in 8K.


What's going to be in this release?

1. A faster divide  Some cycles were removed from the inner loop.

2. Faster Screen Scroll.  The INX INX replacement with LDAB # ABX optimization along with unrolling the loop once.  This saves over 1000 clock cycles per scroll.

3. Faster end of line handling.

4. Faster array handling thanks to a 16x16 bit multiply using the hardware multiply.

5. Some minor optimizations here and there to save space and/or clock cycles.

6. The original parsing routine, CHRGET (common to all Microsoft BASICs), was split between direct page RAM built into the 6803, and ROM.  The original code would increment the memory pointer, load a byte, and then JMP to the 2nd half of the code in ROM.   The code has been updated so the full code is copied to RAM.  Since it extends past the end of the direct page ($00FF), it tests address $0100 to see if the code copied there exists.  If not, it patches the code to JMP to the ROM.  Even if there isn't any expansion RAM at $0100, there was still room on the direct page to handle the most common case/before jumping to ROM.  It's always faster than the factory ROM, but if you have RAM in that address range, it's even faster.


How does the performance compare to the original ROM?

My original goal was a minimum of a 5% speedup across the board, and beating 1 MHz 6502 machines like the Apple II and C64 at Ahl's Benchmark.  Testing has shown the latest version to be about 8% faster on the slowest code, 10% faster on code using arrays but little math, and math intensive things like the 3D "Fedora" plot which uses a lot of multiplication take about 33% less time.  Ahl's Benchmark still sits at about 67 seconds which is where it was after the first use of the hardware multiply, though it's tough to tell with hand timing.  The MC-10 consistently benchmarks faster than 1 MHz 6502 machines running Microsoft BASIC.  Not just at Ahl's Benchmark, but everything.  The speed difference vs the original ROM is now obvious when drawing, printing, etc...

The only thing left to do on this release, is to track down a bug in the error message handling.  The error printing isn't receiving the right error codes.  This is probably due to an optimization that treats the status bits wrong.

Thursday, February 1, 2018

More 6803 optimization

One of the little used speed optimizations when writing 6803 code seems to be the replacement of multiple INX instructions with LDB #xx ABX.
Part of this is due to needing to preserve the contents of B, but because multiple INX instructions may be separated by other LDAA ,X or similar instructions.

There are several places in Microcolor basic where INX INX is used, or even more INX instructios are used.  The INX instruction takes 3 clock cycles and is 1 byte.  LDB #2 requires 2 clock cycles and is 2 bytes.  ABX is 3 clock cycles and 1 byte.  So replacing INX INX with LDB #2 ABX saves 1 clock cycle but takes 1 additional byte.  If the INX INX take place in a little use function, or one that does not impact the normal speed of execution of a program, it makes little sense to use this speed optimization.  But it the INX INX pair are inside a loop, it can save a lot of clock cycles.

Scrolling the text screen is one example.  This is not the actual interpreter's code, but it's close.

; X points to the destination address... $20 is 32, or the length of one line
LOOP:
  LDD $20,X
  STD ,X
  INX
  INX

  CPX  #ENDOFSCREEN-32
  BLT LOOP



The screen contains 32 characters / line * 16 lines, and it copies 2 bytes at a time.
Replacing INX INX with LDB #2 ABX saves 16 * 16, or 256 clock cycles over the entire screen.
However, if you unroll the loop just once, it saves an additional 6 clock cycles per pass, and cuts the number of CPX, BNE, and INX equivalent clock cycles in half!  So savings go from 256 to well over 1000 at the cost of 5 bytes over the original code.  That's at least enough clock cycles to execute another 250 more instructions somewhere else.

LOOP:
  LDD $20,X
  STD ,X
  LDD $22,X
  STD 2,X
  LDB #4
  ABX
  CPX #ENDOFSCREEN-32
  BLT LOOP



That is an obvious case, it is less obvious where the INX instructions are split over many lines of code.  This is from Microcolor BASIC.

;* End of command or program line
LE52A
          inx                           ; advance past the end-of-line terminator 3
          ldaa      ,X                  ; get MSB of 'next line' link 4
          inx                           ; advacne to LSB 3
          oraa      ,X                  ; OR in the LSB of the 'next line' link 4
          staa      ENDFLG              ; clear ENDFLG if end of program 3
          beq       LE589               ; goto END if no more program lines 3
;* Start next program line
          inx                           ; point X to new line number 3
          ldd       ,X                  ; get new line number..
          std       CURLIN              ; ..and store in CURLIN
          inx                           ; advance to LSB of line number 3
          stx       CHRPTR              ; set parser position to start of line -1

This can be replaced with a shorter and faster version.  I had to verify the contents of X and B were not required in LE589 or the code this falls through to, but 11 lines have been replaced with 9, and only 7 are executed most of the time.  4 INX instructions were replaced here.  Savings aren't quite so significant within the scope of the code, but this gets executed at the end of every line of BASIC code, so it adds up over time.

;* End of command or program line
LE52A
ldd 1,X ; get 'next line' link 5
bne LE52B ; zero = no more program lines (the LDD sets flags this tests) 3
staa         ENDFLG ; clear ENDFLG, we are at the end of the program 3
bra LE589 ; goto END if no more program lines 3
LE52B
;* Start next program line
        ldd       3,X                  ; get new line number..
        std       CURLIN              ; ..and store in CURLIN
ldab     #4 ; size of line terminator + next line link + 1 2
abx ; point X to new line number 3
        stx       CHRPTR              ; set parser position to start of line -1



Monday, January 29, 2018

Spending a little more time on the MC-10 ROM

Along with several other projects I'm working on, the MC-10 ROM is getting a little attention again.
It's been a few months, so before porting to the 68hc11, I thought I'd get reacquainted with the code by working on a new release.

The floating point division code has been optimized a little more.  It's pretty minor, but it's in the inner loop, and if you use a lot of division, it and the savings from previous optimizations add up.

The array index to address calculation uses a 16x16 bit multiply, but it does not take advantage of the MUL instruction.  We'll see if the new code offers a noticeable speed improvement.

The code that reduces the number of times the keyboard is scanned was not in the previous release, so that has been added. 

The optimization for storing the pointer to the next line is going to get a look.  That has a lot of potential to speed up complex code.

It would be nice to finish the 16 bit string compare and memory move functions before another release.   Sorting string arrays should be much faster once that's implemented.

Wednesday, December 13, 2017

Another patch to speed up the Plus/4 ROM.

There are a group of functions that the Plus/4 ROM copies to RAM on startup.  Each of these allows access to all of RAM, including under ROM, via different pointers stored on page 0 (the direct page in Motorola terms).

Each of these functions disables interrupts, pages out the ROM, loads A via a pointer on page zero, pages in ROM, enables interrupts, and returns to the caller.  It's a lot of clock cycles to access a single byte.

For programs + data that are small enough to fit in memory without using RAM under ROM, we can patch the ROM to skip the costly sequence of instructions so that it directly accesses memory.

The patch must copy ROM to RAM, set the highest address available to BASIC so that it is before the start address of the ROM, and then install the code listed below.

Each function that is called is replaced with a piece of code that loads A with the page zero address that function uses, then it calls a common patch routine that overwrites the ROM code we copied to RAM so that it directly loads A without the intermediate call.  Each JSR in ROM occupies 3 bytes.  The opcode for JSR, and 2 bytes for the address to call.  The LDA (),Y opcode takes 2 bytes.  So we must overwrite the 3rd byte with a NOP.  Using this approach will result in all calls to the load routines being patched the first time they are called. 

The resulting patched code only requires 6 clock cycles instead of the 29 clock cycles the regular ROM requires.  The beauty of this approach is that it patches every call to these functions without us having to find them all.

Warning: This assumes that the ROM does not use any optimizations where a JMP was used to call the code so that it would eliminate the need for two RTS instructions.  If we discover that technique was used somewhere, we must identify it and place an RTS after the LDA (),Y instead of a NOP.


; stub routine replacing each piece of code ROM calls
LDA #address ; load A with address normally used by LDA (address),Y
JMP PATCH ; call the patcher

; code to patch the ROM where it calls functions to access RAM under ROM
PATCH:
STA #temp ; save page zero address to use
LDY #0 ; zero y

; point to code we want to patch
;  Get LSB of return address from stack and adjust it to address of JSR
PLA ; get LSB of return address
SBC #3 ; subtract 3 (point to address of JSR)
STA RETURNADDRESS ; store it in our own page 0 pointer

;  Get MSB or return address from stack and adjust it if carry set
PLA ; get MSB of return address
BCC NEXT ; deal with carry from MSB
DEC
NEXT:
; Push MSB of JSR address onto the stack
PHA

; patch 1st byte with LDA (),Y opcode
STA RETURNADDRESS+1 ; save it in our pointer
LDA #$B1 ; load the opcode we want to patch with
STA (RETURNADDRESS),Y ; patch BASIC

; patch 2nd byte with address passed from stub routine
INY ; next address 
LDA TEMP ; get the address that was passed to us
STA (RETURNADDRESS),Y ; patch BASIC

; patch 3rd byte with NOP
INY ; next address
LDA #$EA ; NOP to finish the patch
STA (RETURNADDRESS),Y ; patch it

; Push LSB of patched code to stack and call it with RTS
LDA RETURNADDRESS ; get LSB of JSR
PHA ; push it to the stack

RTS ; call the patched code

Thursday, December 7, 2017

Status update of a few projects

The USB serial adapter finally showed up so I can start working on the 68HC11 port of BASIC.  The IDE port is going to be on hold until I get that up and running.

The comments for the VZ disassembly are ready to go, I just need to extract the ones that match the VZ ROM and put them in the sed file.  Then it's just a matter of commenting a few pieces of code VTEC added to the ROM.  One of the things I ran across when benchmarking these old machines is how horribly slow the VZ BASIC is.  Even though it's clocked faster than the TRS-80 Model III and they both share a lot of code, the VZ takes almost twice as long on benchmarks.  The patches VTEC made clearly didn't help it.  Once I convert the disassembly back to a source file, I should be able to run a code profiler on it to see where the biggest bottleneck is and to fix it.  There's plenty of unused space for fixes.


Kicking a dead horse... Commodore Plus/4 style

Here's a little patch to speed up the Commodore Plus/4.  This modifies the RAM based CHRGOT function that is used to scan through the BASIC code.  The standard code disables interrupts, pages out the ROM, reads a byte, pages in the ROM, and enables interrupts for every byte of a program it reads.   It does this so that it can provide up to 60K for BASIC.  This is certainly a nice feature if you need that much RAM, but if you don't, it slows down programs significantly for no reason.

This simple piece of code speeds up one benchmark by about 4%.  It is a pretty significant gain for a few hours work and requires no changes to the ROM.  Getting this much extra speed out of the MC-10 was a lot harder and requires a new ROM.  Actual performance increases will vary by program.  Still, I had hoped for better results.

Only programs that fit in memory below the start address of ROM will work with this as it eliminates the code that pages ROM in and out.  It makes no attempt to modify system variables to restrict code to that area, and it does not restrict the use of upper RAM for data.  Additional patches that restrict data to the same area of RAM would provide additional speed.


Here is the original code used by the Plus/4 BASIC interpreter from a ROM disassembly.  We are most interested in the code starting at $8129 in the ROM.  This is copied to RAM on startup:

        ; CHRGET/CHRGOT - This chunk of code is copied to RAM 
        ; and run from there. It is used to get data UNDER the 
        ; system ROM's for basic.
        ;
; CHRGET ($0473)
L8123   INC   LastBasicLineNo         ; $3b (goes to $0473 ) CHRGET
        BNE   L8129
        INC   LastBasicLineNo+1       ; $3c
;
; CHRGOT ($0479)
;
L8129   SEI    
        STA   RAM_ON
        LDY   #$00
        LDA   (LastBasicLineNo),y     ; $3b
        STA   ROM_ON
        CLI    
        CMP   #$3A   ; ":" (colon)
        BCS   L8143   ; if colon, exit
        CMP   #$20   ; " " (space)
        BEQ   L8123   ; if space, get NEXT byte from basic
        SEC    
        SBC   #$30
        SEC    
        SBC   #$D0
L8143   RTS    



This contains the new CHRGOT function.  It's code was embedded in the BYTE section of the patch that follows this listing.  Note that code is designed to exit without any branches for the most commonly found type of byte.  This saves a clock cycle for every such byte as branch taken requires one more clock cycle than not taken.

00000r 1                .ORG $0473
000473  1               
000473  1                ;
000473  1                ; CHRGET/CHRGOT - This chunk of code is copied to RAM
000473  1                ; and run from there. It is used to get data UNDER the
000473  1                ; system ROM's for basic.
000473  1                ;
000473  1                ; CHRGET ($0473)
000473  1               L8123:
000473  1  E6 3B         INC LastBasicLineNo ; $3b (goes to $0473 ) CHRGET
000475  1  D0 02         BNE L8129
000477  1  E6 3C         INC LastBasicLineNo+1 ; $3c
000479  1                ;
000479  1                ; CHRGOT ($0479)
000479  1                ;
000479  1               L8129:
000479  1  A0 00         LDY #$00
00047B  1  B1 3B         LDA (LastBasicLineNo),y ; $3b
00047D  1  C9 3A         CMP #$3A ; Larger than $3A?
00047F  1  90 01         BCC NEXT ; if not, skip to NEXT
000481  1  60            RTS ; return if so
000482  1               NEXT:
000482  1  E9 2F         SBC #$2F ; A=A-$30
000484  1  C9 F0         CMP #$F0 ; Is it a " "? (space)
000486  1  F0 EB         BEQ L8123 ; if space, get NEXT byte from basic
000488  1  38            SEC ; A=A-$D0
000489  1  E9 D0         SBC #$D0 ; clear carry if digit, set otherwise
00048B  1  60            RTS
00048C  1               
00048C  1                .end



This is the source code for the program that patches the CHRGOT function.  It is designed to be embedded in a REM statement in the first line of a BASIC program.  Note that the 2nd byte of the actual CHRGOT code has been changed from $00 to $01 and is patched once it is copied to it's final destination.  Microsoft BASICs don't advance to the next line by using the pointer stored at the start of the line once it starts to parse a line.  It scans for the end of line marker which is $00.  It assumes anything that follows is a line of BASIC code.  Storing the byte as non $00 is required so BASIC can skip to the next line every time the program runs.

000000r 1               
000000r 1                .org $1006 ; The address of ML$ in our BASIC program
001006  1               
001006  1  A0 12         LDY #18 ; Starts at CHRGOT+18 and works down...
001008  1               NEXT:
001008  1  B9 17 10      LDA CHRGOT,Y ; Get byte of new CHRGOT routine
00100B  1  99 79 04      STA $0479,Y ; Save it over the old routine
00100E  1  88            DEY ; decrement our loop counter/index register
00100F  1  10 F7         BPL NEXT
001011  1  C8            INY
001012  1  98            TYA
001013  1  99 7A 04      STA $047A,Y
001016  1  60            RTS
001017  1               CHRGOT:
001017  1  A0 01 B1 3B   .BYTE $A0,$01,$B1,$3B,$C9,$3A,$90,$01,$60,$E9,$2F,$C9,$F0,$F0,$EB,$38,$E9,$D0,$60
00101B  1  C9 3A 90 01  
00101F  1  60 E9 2F C9  
00102A  1                .end


This is the final BASIC code containing the patch.  It can be added to smaller programs to sped them up.  After the first time the program has been run, the lines containing the DATA statements, and line 1 can be deleted.  The resulting program can be saved with the patch permanently embedded in the REM statement.

0 REM012345678901234567890123456789012345
1 FORI=0 TO 35:READ T:POKE 4102+I,T :NEXT I
2 SYS 4102

10000 DATA 160,18,185,23,16,153,121,4,136,16,247,200,152,153,122,4,96
10010 DATA 160,01,177,59,201,58,144,1,96,233,47,201
10020 DATA 240,240,235,56,233,208,96



Here is the benchmark that prompted me to write this.

10 K=0:I=0:T=0:P=0
30 SCNCLR
100  PRINT "Prime Number Generator"
110  INPUT "Upper Limit";N

120  eTime=TIME
130  T=(N-3)/2
140  DIMA(T+1)

160 FORI=0TOT:A(I)=0:NEXT
200 FORI=0TOT:IFA(I)THENPRINT"..";:NEXT:GOTO330
210P=I+I+3:PRINTP;".";:K=I+P:IFK<=TTHENFORK=KTOTSTEPP:A(K)=1:NEXT:NEXT:GOTO330
260 NEXT

330  eTime=(TIME-eTime)/60
340  PRINT
350  PRINT "Total: ";eTime
360 END


This will speed up the benchmark by over 30%.  It disables the screen refresh while the benchmark is running.  The screen refresh normally steals that many clock cycles away from the CPU.
115 POKE65286,PEEK(65286)AND239
335 POKE65286,PEEK(65286)OR16