Introduction

Sometimes it is needed some extra speed in ASM or make your game smaller to fit on the calculator. Examples: consuming graphics/data programs and graphics code of mapping, grayscale and 3D graphics.

Registers and Memory

Generally good algorithms on z80 use registers in a appropriate form.
It is also a good practise to keep a convention and plan how you are going to use the registers.

General use of registers:

a - 8-bit accumulator

b - counter

hl - 16-bit accumulator/pointer of a address memory

de - pointer of a destination address memory

bc - 16-bit counter

ix - index register/save copy of hl/pointer to memory when hl and de are being used

Stack

When you run out of registers, stack may offer an interesting alternative to fixed RAM location for temporary storage.

Allocation

You can either allocate stack space with repeated push, which allows to initialize the data but restricts the allocated space to multiples of 2.
An alternate way is to allocate uninitialized stack space (hl may be replaced with an index register) :

Access

The most common way of accessing data allocated on stack is to use an index register since all allocated "variables" can be accessed without having to use inc/dec but this is obviously not a strict requirement. Beware though, using stack space is not always optimal in terms of speed, depending (among other things) on your register allocation strategy :

If your needs go beyond simple load/store however, this method start to show its real power since it vastly simplify some operations that are complicated to do with fixed storage location (and generally screw up register in the process).

Again, choose wisely between hl and an index register depending on the structure of your data the smallest/fastest allocation solution may vary (hl equivalent instructions are generally 2 bytes smaller and 12 T-states faster but do not allow indexing so may require intermediate inc/dec).

Deallocation

If you want need to pop an entry from the stack but need to preserve all registers remember that sp can be incremented/decremented like any 16bit register :

Shadow registers can be of a great help but they come with two drawbacks :

they cannot coexist with the "standard" registers : you cannot use ld to assign from a standard to a shadow or vice-versa. Instead you must use nasty constructs such as :

; loads hl' with the contents of hl
push hl
exx
pop hl

they require interrupts to be disabled since they are originally intended for use in Interrupt Service Routine. There are situations where it is affordable and others where it isn't. Regardless, it is generally a good policy to restore the previous interrupt status (enabled/disabled) upon return instead of letting it up to the caller. Hopefully it s relatively easy to do (though it does add 4 bytes and 29/33 T-states to the routine) :

ld a, i ; this is the core of the trick, it sets P/V to the value of IFF so P/V is set iff interrupts were enabled at that point
push af ; save flags
di ; disable interrupts
; do something with shadow registers here
pop af ; get back flags
ret po ; po = P/V reset so in this case it means interrupts were disabled before the routine was called
ei ; re-enable interrupts
ret

General Algorithms

Registers and Memory use is very important in writing concise and fast z80 code. Then comes the general optimization.

First, try to optimize the more used code in subroutines and large loops. Finding the bottleneck and solving it, is enough to many programs.

A list of things to keep in mind:

Rework conditionals to be more efficient.

Make sure the most common checks come first. Or said in other way, the more special and rare cases check in last.

Get out of the main loop special cases check if they aren't needed there.

Rearrange program flow

When possible, if you can afford to have a bigger overhead and get code out of the main loop do it.

When your code seems that even with optimization won't be efficient enough, try another approach or algorithm. Search other algorithms in Wikipedia, for instance.

Rewriting code from scratch can bring new ideas (use in desperate situations because of all work needed to write it)

Remember almost all times is better to leave optimization to the end. Optimization can bring too early headaches with crashes and debugging. And because ASM is very fast and sometimes even smaller than higher level languages, it may not be needed further optimization.

Document wacky optimizations to understand the code later

Small Tricks

Note that the following tricks act much like a peep-hole optimizer and are the last optimization step : remember to first optimize your algorithm and register allocation before applying any of the following if you really want the fastest speed and the smallest code.

Also note that near every trick turn the code less understandable and documenting them is a good idea. You can easily forgot after a while without reading parts of the code.

Be warned that some tricks are not exactly equivalent to the normal way and may have exceptions on its use, comments warn about them. Some tricks apply to other cases, but again you have to be careful.

There are some tricks that are nothing more than the correct use of the available instructions on the z80. Keeping an instruction set summary, help to visualize what you can do during coding.

Size vs. Speed

The classical problem of optimization in computer programming, Z80 is no exception.
In ASM most frequently size is what matters because generally ASM is fast enough and it is nice to give a user a smaller program that doesn't use up most RAM memory.

For the sake of size

Use relative jumps (jr label) whenever possible. When relative jump is out of reach (out of -128 to 127 bytes) and there is a jp near, do a relative jump to the absolute one. Example:

;lots of code (more that 128 bytes worth of code)
somelabel2:
jp somelabel
;less than 128 bytes
jr somelabel2 ;instead of a absolute jump directly to somelabel, jump to a jump to somelabel.

Relative jumps are 2 bytes and absolute jumps 3. In terms of speed jp is faster when a jump occurs (10 T-states) and jr is faster when it doesn't occur.

This routine can be expanded to pass the coordinates where the text should appear.

Wasting time to delay

There are those funny times that you need some delay between operations like reads/writes to ports and there is nothing useful to do. And because nop's are not very size friendly, think of other slower but smaller instructions. Example:

;Instead of
ld a,KEY_GROUP
out (1),a
nop
nop
in a,(1)
;Try this:
ld a,KEY_GROUP
out (1),a
ld a,(de) ;a doesn't need to be preserved because it will hold what the port has.
in a,(1)
; -> save 1 byte and 1 T-state (well 1 T-state less is almost the same time)

When you need to delay and cannot afford to alter registers or flags there are still ways to delay that waste less size than nop's :

For delay between frames of games or other longer delays, you can use the 'halt' instruction if there are interrupts enabled. It make the calculator enter low power mode until an interrupt is triggered. To fine-tune the effect of this delay mechanism you can alter interrupt mask and interrupt time speed beforehand (and possibly restore their values afterwards).

Unrolling code

General Unrolling
You can unroll some loop several times instead of looping, this is used frequently on math routines of multiplication.
This means you are wasting memory to gain speed. Most times you are preferring size to speed.

Unroll commands

; "Classic" way : ~21 T-states per byte copied
ld hl,src
ld de,dest
ld bc,size
ldir
; Unrolled : (16 * size + 10) / n -> ~18 T-states per byte copied when unrolling 8 times
ld hl,src
ld de,dest
ld bc,size ; if the size is not a multiple of the number of unrolled ldi then a small trick must be used to jump appropriately inside the loop for the first iteration
loopldi: ;you can use this entry for a call
ldi
ldi
ldi
ldi
ldi
ldi
ldi
ldi
jp pe, loopldi ; jp used as it is faster and in the case of a loop unrolling we assume speed matters more than size
; ret if this is a subroutine and use the unrolled ldi's with a call.

This unroll of ldi also works with outi and ldr.

Looping with 16 bit counter

There are two ways to make loops with a 16bit counter :

the naive one, which results in smaller code but increased loop overhead (24 * n T-states) and destroys a

ld bc, ...
loop:
; loop body here
dec bc
ld a, b
or c
jp nz,loop

the slightly trickier one, which takes a couple more bytes but has a much lower overhead (12 * n + 14 * (n / 16) T-states)

As you can see these are extremely simple, small and fast ways to alter flags
which make them interesting as output of routines to indicate error/success or
other status bits that do not require a full register.

Were you to use this, remember that these flag (re)setting tricks frequently
overlap so if you need a special combination of flags it might require slightly
more elaborate tricks. As a rule of a thumb, always alter the carry last in
such cases because the scf and ccf instructions do not have side effects.