I gather the fastest way to implement memcpy (copy a certain number of bytes from one place in memory to another) on the Z80 is to use an instruction called LDIR. But how fast is the result, when fully optimized, in terms of clock cycles per byte?
4 Answers
There's no real optimisation — LDIR
(or indeed LDDR
, which goes downward instead of upwards) is the complete inner loop. It will always load from HL, store to DE, increment both and decrement BC. Then if BC is non-zero it will repeat.
Annoyingly it will repeat exactly by just decrementing the PC by 2. So it'll read the full instruction again. Which means that 50% of memory accesses are it reading the opcode for the entire loop. The advantage is that interrupts can be accommodated while a transfer is in progress.
Cost is 21 cycles for each time around the loop that does lead to a repetition, then 16 cycles on the final go. Four cycles opcode fetch, four cycles opcode fetch, three cycles reading from the address pointed to by HL, then a long five cycle write to the address pointed to by DE. Then five cycles without a bus access if repeating.
Unrolled LDIs will be faster if you have the space as they always cost the flat 16 cycles. E.g. put 16 of them in a row with a conditional jump at the end and if the number of bytes you want to copy is n then jump into the loop n mod 16 steps from the repetition test for the first iteration.
Alternatively, if you're writing for a computer with slower RAM access that ROM access (e.g. because RAM is shared with video) then a top tip is to look for the three-byte sequence LDIR RET
anywhere in ROM and call it.
-
6The 6502 uses only half of each cycle for memory access, which means that memory has to be twice as fast as teh clock rate would suggest. So a 1 MHz 6502 needs >2 MHz RAM (better than 500 ns), but uses only half the bandwith, leaving between each access enough time for another one to be used by video (or otherwise). The Z80 instead doesn't run with such a symetric access scheme offering bontifull time to insert 'hidden' video access. Here video needs to be priortized over CPU, claiming the RAM when needed, thus 'stealing' cycles. We're at the heart of the decade old 6502 MHz vs. Z80 MHz topic.– RaffzahnCommented Sep 16, 2017 at 8:34
-
7Thats also the reason why (at least somewhat) professional sysems always include a Z80 DMA (8410). A 4MHz Z80 can transfers a bit less than 200 KiB/s (unrolling gives ~250 KiB/s), as only 4 of 21 (16) cycles are memory access (half of it being instruchtion (re)fetch) me while the Z80 DMA uses the full bandwith, offering up to 2 MiB/s - all cycles are now used for data transfer. Ofc, minus video cycles, if that unit is also accessing the RAM.– RaffzahnCommented Sep 16, 2017 at 8:54
-
6If interrupts including NMI can be disabled, it's possible to write code that outperforms
LDIR
by using a sequence of stack manipulations, pops, register swaps, and pushes.LDIR
is a lot easier, though. I do find myself curious how hard it would have been to make the microcode loop when no interrupt happens.– supercatCommented Sep 16, 2017 at 18:26 -
2@rwallace My gut says right after the CPU. But I can't proof that. I don't have realy old documentation at hand. But from whats on my desk, it's already listed in the 1981 databook and a February 1980 datasheet - as well as the 1981 application reference. Furthermore I got a Z80 Family data book with no date (listing the 8410) which I assume to be eventually before 1979, as it refers to the 1979 databook in future form. Ao I go with anywhere between 1976 and 1978.– RaffzahnCommented Oct 7, 2017 at 14:04
-
2@rwallace I found a high quality scan of the Feb 1980 data sheet online: bitsavers.org/components/zilog/z80/…– RaffzahnCommented Oct 7, 2017 at 14:06
You can do better in particular cases. Here is an inner loop (REPT 11
[...] ENDM
makes 11 inline repetitions) for scrolling by 16 bits in a screen area I used in a game of mine:
LD B,16 ;29
SCRL11: EX DE,HL ;4
LD E,(HL) ;11
INC HL ;17
LD D,(HL) ;24
INC HL ;30
EX DE,HL ;34
REPT 11
EX (SP),HL ;35
DEC SP ;41
DEC SP ;47
ENDM ;551
EX (SP),HL ;586
LD HL,22+40H ;596
ADD HL,SP ;607
LD SP,HL ;611
DJNZ SCRL11 ;10008
Numbers are accumulated clock cycles (obviously the first line does not start with zero: this is part of something quite longer). I am not actually sure why EX (SP),HL
clocks in with 35 cycles here: the nominal count is 19 cycles. I'm assuming that the 16 additional cycles are wait states because access is in video memory.
Looks like I'm a bit fuzzy after somewhat more than a quarter century.
In general, putting the stack pointer in the data area can improve speed for some operations since LDI
and its ilk only work by bytes and have extended Z80 opcodes (where the opcode fetch alone is good for something like 8 cycles) while many stack operations work by words and are part of the original 8080 opcode set. Of course, you cannot have interrupts arriving while doing that kind of thing.
-
3
-
I wonder how much silicon it would have cost to have an internal flag that would skip instruction fetch cycles,would be set when LDIR etc. reset the program counter, and would be cleared when either LDIR didn't go to reload the program counter or when an interrupt occurred? Even if getting LDIR down to 6 cycles/byte would be expensive, its present form seems really unnecessarily slow.– supercatCommented May 6, 2018 at 17:06
The heaven of memcpy
-like optimization in Z80 is the stack.
If you have destination fixed, for example, you do like:
ld sp,src
pop hl
ld [dest+0],hl
pop hl
ld [dest+2],hl
...
thus getting 14 clocks per byte transferred (ldir/lddr
is just 21 clocks per byte).
If both source and destination are fixed, you can do:
ld sp,src
pop af,bc,de,hl
exx
ex af,af`
pop af,bc,de,hl
ld sp,dest+16
push hl,de,bc,af
exx
ex af,af`
push hl,de,bc,af
...
thus getting ~13 clocks per byte.
The main disadvantage of such stack methods is the need to disable interrupts (in general case, of course there could be cases when interrupt is acceptable).
-
6With no loop test in your first snippet, the correct comparison is
LDI
, notLDIR
. So having a fixed destination saves 2 cycles, not 7.– TommyCommented Sep 16, 2017 at 20:21
Perhaps I should just edit the contradiction out of the accepted answer that starts by claiming you can do no better than LDIR
then mentions that unrolling it to a series of LDI
instructions is faster.
But for the nonce I'll put an explicit example of the LDI
unroll here.
; Copy BC bytes from HL to DE.
copy_mem:
ld a,b
or c
ret z
ld a,16
sub c
and 15
add a,a
ld (lp_entry),a
jr $
lp_entry equ $-1
copy_lp:
ldi
ldi
ldi
ldi
ldi
ldi
ldi
ldi
ldi
ldi
ldi
ldi
ldi
ldi
ldi
ldi
jp v,copy_lp
ret
The code does check for the byte count == 0 case. However, it does use self-modifying code so it won't work if run from ROM nor will it be viable in a multi-threaded environment. Replacing the unrolled loop entry with a jump table is left as an exercise for the reader.
Note that while LDI
does not loop it does decrement BC
and sets the overflow flag to indicate that BC != 0
. That helps make the loop faster and by unrolling by 16 the cost per byte is 16 + 10/16 cycles: 16 cycles per LDI
which copy a byte and a 10 cycle cost every 16 bytes to account for the JP V
.
Can We Do Better?
Well, Z-80 memory access is fastest through the PUSH
and POP
instructions. PUSH
can write 2 bytes in 11 cycles and POP
can read 2 bytes in 10 cycles. Thus the theoretical maximum speed write is 5.5 cycles/byte and maximum speed read is 5 cycles/byte. This means that 10.5 cycles/byte is the best we could possibly do. Loop overhead will cut that number down, of course.
The main issue with using PUSH
and POP
is that you cannot have any interrupts occur while the copy takes place. That may be a deal breaker, but if acceptable wrapping the function in DI
/EI
will take care of maskable interrupts and we'll assume that specific platform knowledge can rule out non-maskable interrupts (NMI).
The secondary problem is that between loading the stack pointer register twice for each block copied and some counter loop overhead it is hard if not impossible to run faster than 16 cycles/byte so LDI
wins.
If your source and destination are fixed a fully unrolled stack-based copy can get as low as 12.5 cycles per byte. And the unrolling costs 1.57 program bytes per byte moved. Those are pretty heavy restrictions but there are applications. For example, moving bytes from an off-screen frame buffer to the hardware buffer.
Here's one iteration of such an unroll that moves 14 bytes:
ld sp,src ; 10
pop af ; 10
pop bc ; 10
pop de ; 10
pop hl ; 10
exx ; 4
pop bc ; 10
pop de ; 10
pop hl ; 10
ld sp,dst ; 10
push hl ; 11
push de ; 11
push bc ; 11
exx ; 4
push hl ; 11
push de ; 11
push bc ; 11
push af ; 11
Those numbers add up to 175 cycles. 175 cycles / 14 bytes = 12.5 cycles/byte.
Note that it is perfectly safe to use AF
in this fashion; the data will be copied correctly. Also note that trying to speed it up by using IX
or IY
won't work as their PUSH/POP
is slower and can only manage 14.5 cycles/byte.
-
1George, you might want to add that above is only true for fixed adresses of src and dst (and dst being destination+14) and only works if only 14 bytes are to be transfered. With variable addresses LD SP,(x) counts 20 clocks (may be overcome with self modifying code but only in RAM) and any transer of more than 14 bytes adds an overhead of at least 61 cycles for pointer update plus 20 for the loop, for a total of 256 cycles, or ~18,3 cycles, thus beeing slower than an unroaled LDI– RaffzahnCommented Sep 16, 2017 at 21:28
-
@George your first paragraph shows that you've misunderstood the question. Directly to quote it: "I gather the fastest way to implement memcpy (copy a certain number of bytes from one place in memory to another) on the Z80 is to use an instruction called LDIR. But how fast is the result, when fully optimized, in terms of clock cycles per byte?" So it asks about optimising an
LDIR
implementation. It is correct to say that anLDIR
implementation doesn't need optimising. It is then an extension to discuss alternatives, but I felt it helped more fully to answer "But how fast is the result?"– TommyCommented Sep 17, 2017 at 0:44 -
2I think the question was just poorly worded. Taking your interpretation it is a non-sequitur. You can't optimize a single instruction. Commented Sep 17, 2017 at 1:09
16T/Byte
which is for3.547 MHz
a221687.5 Byte/s
without contentionmemcpy
(as demonstrated in the answers), but it certainly makes compact code.