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?
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.
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.
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).
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
Can We Do Better?
Well, Z-80 memory access is fastest through the
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
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
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
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
IY won't work as their
PUSH/POP is slower and can only manage 14.5 cycles/byte.