Based on the above, the utter maximum theoretical copy rate is half a word (of native register size) per clock cycle, or equivalently two clock cycles per native register size word coped. (For string operations, two cycles per byte copied, unless you have a branch instruction "branch if none of the bytes in the target register are zeroes".)
It's not that bad. The following will detect a null byte in a 64 bit register without any special instructions.
#define DETECTNULL(X) (((X) - 0x0101010101010101) & ~(X) & 0x8080808080808080)
Example
X = 'Hello\0xx' = 0x48454c4c4f007878
W = X - 0x0101010101010101 = 0x47444b4b4dff7777
V = ~X = 0xb7bab3b3b0ff8787
U = W & V = 0x700030300ff0707
T = U & 0x8080808080808080 = 0x0000000000800000
T is not 0, so some byte in X was 0.
This is only four instructions, or three instructions if your CPU has a BIC / ANDN / ANDC instruction (as RISC-V does in the B extension, and also ARM, MSP430, PDP-11 and VAX).
You can find the exact byte using CLZ (Count Leading Zeroes) if you have it, otherwise just iterate through that one register worth of data.
However I wanted to improve on this in the RISC-V B extension, but without doing something as crude as a special "strlen" function.
We already were considering a "Generalized Reverse" (GREV) instruction which takes a 64 bit value from a register and passes it through a 6-level network controlled by 6 bits from the 2nd operand (5 bits for a 32 bit machine):
- bit 0 controls whether pairs of bits in the 1st operand are swapped
- bit 1 controls whether bits 2 apart in each nybble are swapped
- bit 2 controls whether bits 4 apart in each byte are swapped
- bit 3 controls whether bits 8 apart in each short are swapped
- bit 4 controls whether bits 16 apart in each word are swapped
- bit 5 controls whether bits 32 apart are swapped
Some uses:
- a control value of 000000 does nothing
- a control value of 000111 reverses the bits in each byte
- a control value of 111000 reverses the bytes in a register, keeping each individual byte intact (i.e. reverse a string)
- a control value of 111111 reverses all the bits in the register
I realized that another useful instruction (which were are calling GORC for now) could reuse the same network, but instead of conditionally swapping pairs of bits, use another function. Specifically, GORC takes each pair of bits and either passes them through unchanged or else replaces both or them by their OR. You can get GREV+GORC for maybe 10% more than the cost of GREV alone.
- a control value of 000000 does nothing
- a control value of 000111 ORs together all the bits in each byte
Bingo!
Taking our X = 'Hello\0xx' = 0x48454c4c4f007878 example again, GORC X, 7 produces 0xffffffffff00ffff
If the result is not all 1s (-1) then one of the bytes was zero.
Because we cannot create an unrolled version for every single length, up to one cycle per iteration is taken by the loop branch. As brucehoult showed earlier, that overhead depends on the number of unrolled copies per loop iteration – but then the block size needs to be at least as large as the copy step.
That leaves the address register updates. With offset addressing (a small immediate constant, a multiple of the native register size, added to a register), that too boils down to one or two cycles per loop iteration (depending on whether updating a register value can be done in a single cycle or not). If the register update can be done in parallel with a load or store or the loop branch, its "cost" vanishes.
It seems to me that both block-memcpy instruction and a normal assembly loops should be able to get arbitrarily close to two cycles per native word copied (using the above assumptions), and that anything better than this, you need e.g. wider single-cycle memory accesses, or something else outside the box here, so to speak. If you agree, then shouldn't we look at which low-level operations can be done in parallel in the same clock cycle on your processor design, to see how to reach the two cycles per word copy rate? And what kind of instructions would get there?
Seems all correct.
I think a dual issue pipeline will be the minimum within a few years, for all but the tiniest embedded cores, as it is *so* little added area/complexity/expense for a large gain in efficiency See ARM M7 / A7 / A8 / A9, SiFive 7-series, Western Digital SweRV (open sourced).
With a dual issue machine, you only need to unroll the loop once to completely hide the update of two pointers, a compare against a limit, and a conditional branch. (the compare and branch are one instruction on RISC-V)
add limit,src,size
loop:
lw tmp, 0(src); addi src,src,8
sw tmp 0(dst); addi dst,dst,8
lw tmp -4(src); cmp src,limit
sw tmp -4(dst); blt loop