8
\$\begingroup\$

There's only one question for machine-language golfing, and it's for the ia32 and amd64 architectures. Here's one for a predecessor of them: the Zilog Z80!

The ISA is available here.

As usual, if your tip is only applicable to a certain environment/calling convention, please indicate that in your answer.

Only one tip per answer (see here).

You can also offer tips for the closely related Sharp SM83. The ISA for that is available here.1

Please mark your tips as to whether they're applicable to Z80, SM83, or both; the ISAs might be similar, but there are many differences.

  1. It's labeled as the LR35902; that's the SiC that was used in the DMG and CGB. It had an SM83 core.
\$\endgroup\$

8 Answers 8

3
\$\begingroup\$

Shadow registers

When you're short on registers, you can use the shadow registers - af', bc', de' and hl'. These registers map directly to their usual counterparts (without '). You can exchange registers using ex reg, reg' or exx. The later exchanges all registers (af and af', bc and bc', etc...). Drawbacks:

  • not compatible with "usual" registers, you can't use for example ld to assign from a standard to a shadow register.
  • they were originally meant to be used inside ISRs, so the interrupts should be disabled while you use them.
\$\endgroup\$
1
  • \$\begingroup\$ Note that ex reg,reg' has the only variant ex af,af' (and a few others, but they are not shadow registers), and exx doesn't exchange af/af', just the others. Good suggestion anyway. \$\endgroup\$
    – FZs
    Jun 25, 2021 at 19:18
2
\$\begingroup\$

Use ldir and friends.

Instead of many successive loads, from the same location to a consecutive block of memory, you can use ldir:

    ld (mem),a
    ld (mem+1),a
    ld (mem+2),a
    ld (mem+3),a
    ld (mem+4),a
    ld (mem+5),a

... becomes:

    ld hl,mem
    ld de,mem+1
    ld (hl),a
    ld bc,5
    ldir

If you're familiar with x86 assembly, ldir is somewhat comparable to rep movsb. Of course, z80 has many more instructions like that: cpir, inir, otir, lddr, cpdr, indr, otdr

\$\endgroup\$
2
\$\begingroup\$

cp and cpl-related tricks

cp 0

cp 1

become respectively

or a

dec a

... saving a single byte. You can also use cpl instead of xor $FF to flip bits in a byte.

You can also use cpl for subtracting accumulator from some constant:

cpl
add a, X+1

... is one byte shorter than neg and add a, X

\$\endgroup\$
2
\$\begingroup\$

Messing with flags

You can do the following flag operations in a compact way:

  • Carry flag: setting - scf, resetting - or a, alternatively and a (modifies sign and zero).
  • Zero flag: setting - cp a (resets carry, modifies sign), resetting - or 1 (resets carry, modifies a and sign).
  • Sign flag: setting - or $80 (reset zero and carry, modifies a), resetting - xor a (clear a, set zero, clear carry).
  • Half carry: and a - set, xor a - reset.
\$\endgroup\$
2
\$\begingroup\$

Here's a tip to start off: jp is generally a bad idea.

Unless your code is more than 128 bytes long (which it probably won't be), you can definitely save a byte with a jr. As a bonus, your code will be position-independent.

Just one quick warning: the offset of jr is from the start of the instruction on Z80 (so 18 00 is an infinite loop), but the end on SM83 (so 18 00 is a two-byte three-cycle no-op).

Therefore, jr on SM83 can offset the IP by any amount from -126 to 129, but on Z80 it's -128 to 127.

\$\endgroup\$
1
\$\begingroup\$

Some of the most useful commands for golfing in Z80 assembly language are sbc and adc. They often allow you to interact with the flag C in ways that are absolutely amazing. Here are some cool examples:

  1. How to create a decreasing counter in A that stops at 0? or a : jr nz,$+3 : dec a is ugly. sub 1 : adc 0 is faster, runs in constant time and can often be size-optimized, if you have handy 0s or 1s in one of your other registers. Have to stop at a non-zero value N? No problem: sub N : adc N-1. This also works for incrementing counter that is clamped at 255: add 1 : sbc 0. This also works for a incrementing counter clamped at an arbitrary value N: add 256-N : sbc 256-N-1.

  2. How to load an arbitrary value N to A when flag C is up (or zero otherwise)? ld a,N : jr c,$+3 : xor a is not the way. sbc a : and N usually is (it is faster and shorter by 2 bytes).

  3. How about loading into A value N if flag C is up or value M if flag C is down? Don't mess with conditional jumps! Use sbc a : and N-M : add M. (This last trick I believe is invented by GriV).

\$\endgroup\$
1
\$\begingroup\$

Another simple one:

Use xor a or sub a instead of ld a,0 to reset (load 0 to) the a register.

Their use seems to be controversial in non-golfing situations (some sources use them, some advise against them), but I see no point in not using them:

  • They are 1 byte, while an immediate load is two bytes.
  • They are also faster (not generally a consideration for ): they take 4 T-states, while an equivalent immediate load takes 7 T-states (8 on SM83).

They have a single side-effect: they modify the flags, while a load does not. You can play around with this if you also want some flags to be set/reset.

On Z80, the flags are affected as follows:

Flags: S Z H P/V N C
xor a 0 1 0 1 0 0
sub a 0 1 0 0 1 0
\$\endgroup\$
6
  • \$\begingroup\$ (1 versus 2 CPU cycles, for the SM83.) \$\endgroup\$ Jun 25, 2021 at 15:50
  • \$\begingroup\$ @NoLongerBreathedIn According to the table you've attached, it's 4 vs 8. The 1 vs 2 is the length in bytes... \$\endgroup\$
    – FZs
    Jun 25, 2021 at 15:54
  • \$\begingroup\$ That table lists cost in clock cycles, not CPU cycles, which each take four clock cycles (or eight on the CGB until you poke to a specific register). \$\endgroup\$ Jun 25, 2021 at 19:52
  • \$\begingroup\$ @NoLongerBreathedIn Ok, I don't know the technical details of SM83, I know only Z80... \$\endgroup\$
    – FZs
    Jun 25, 2021 at 19:57
  • \$\begingroup\$ Surely sub a generates no half carry or overflow? \$\endgroup\$
    – Neil
    Jul 1, 2021 at 9:52
0
\$\begingroup\$

Use jp instead of call and ret.

In some cases, jp is viable (contrary to the other answer). call procedure / ret can in some cases be replaced with jp procedure, saving one byte.

\$\endgroup\$
1
  • \$\begingroup\$ True, but what I meant was that one should use jr instead of jp whenever possible. \$\endgroup\$ Jun 25, 2021 at 19:56

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.