There is an interesting point about example code for beginners (or the lazy), vs the actual production code in the standard library.

For example the strcpy:

void stringcopy(char *dst, const char *src) {
int i;
char c;
do {
c = *src++;
*dst++ = c;
} while (c != '\0');
}

With asm code (I've changed it to use the normal pseudo-ops):

.section .text
.global stringcopy
stringcopy:
# a0 = destination
# a1 = source
1:
lb t0, 0(a1) # Load a char from the src
sb t0, 0(a0) # Store the value of the src
beqz t0, 1f # Check if it's 0
addi a0, a0, 1
addi a1, a1, 1
j 1b
1:
ret

For some reason they've made the assembly language not actually a direct translation of the C code. Ironically, this has actually slowed it down. On a typical single-issue in-order core (which everything in RISC-V land is so far, except the SiFive U74 in the upcoming HiFive Unmatched and Beagle-V) this will take 7 clock cycles per byte copied, as the sb will stall for 1 cycle waiting for the lb.

If they'd at least put the first addi between the lb and sb that would save a cycle. But keeping it organized the same as the C code would reduce it to 5 clock cycles per byte:

.section .text
.global stringcopy
stringcopy:
# a0 = destination
# a1 = source
1:
lb t0, 0(a1) # Load a char from the src
addi a0, a0, 1
sb t0, 0(a0) # Store the value of the src
addi a1, a1, 1
bneqz t0, 1b # Repeat if not 0
ret

That's shorter and faster for any string with at least one character before the NULL. (I'm ignoring branch prediction here as it will affect both equally)

So there you have 40% faster code just by sticking more closely to the C code.

The author has called this function stringcopy not strcpy which is probably a good thing because it doesn't meet the contract for strcpy -- the return value for strcpy is the start of the destination buffer i.e. return with a0 unchanged from how you found it. The code should copy a0 to somewhere else ... anything from a2..a7 or t1..t6 (since t0 is already used) and then work with that register instead of a0.

Real strcpy code in libc is much more complex because it tries to copy a whole register (8 bytes) each loop, which means you want to initially get the src and/or dst pointers aligned to a multiple of 8 and then also do some shifting and masking each iteration if the src and dst are not aligned the same as each other. And you also have the problem of detecting a zero byte in the middle of a register. It's also important if the string is near the end of a memory page not to try to read a few bytes of the next page, as you might not have access rights for it.

You quickly find you have hundreds of bytes of code for an optimised strcpy.

The current RISC-V glibc code simplifies the problem by calling strlen first, which depends only on the src, and then using optimised memcpy for the actual copy and ends up running at about 1.5 clock cycles per byte copied on long strings. Which is better than 5 or 7.

ARM and x86 strcpy improve on this by using NEON / SSE / AVX to copy more at a time, but they still need rather long and complex code to deal with alignment issues, and scalar code to deal with odd-sized tails.

The new RISC-V Vector extension gives a huge improvement for all these issues.

Version 1.0 of the Vector extension is not ratified yet (will probably happen in June or July) and there are no chips out using it, but Allwinner now have an SoC called "D1" using the C906 core from Alibaba/T-Head, and it has a Vector unit implementing the 0.7.1 draft version of the RISC-V Vector extension.

In some ways this is unfortunate, as the 1.0 spec is not in general compatible with the 0.7.1 spec. Some simple code is binary compatible between them, and the structure of how you write loops etc is the same, but some instruction semantics and opcodes have changed (for the better).

I currently have ssh access to a EVB (EValuation Board) from Allwinner in Beijing and expect to have my own board here in New Zealand early next month. Sipeed and Pine64 will have mass-production boards in a couple of months. Sipeed have promised a price of $12.50 for at least one version (probably with 256 or 512 MB of RAM I think) and Pine64 have said "under $10". The clock speed of this Allwinner D1 is 1.0 GHz.

Here is vectorized strcpy code I've tested on the board:

# char\* strcpy(char *dst, const char* src)
strcpy:
mv a2, a0 # Copy dst
1: vsetvli x0, x0, e8,m4 # Vectors of bytes
vlbuff.v v4, (a1) # Get src bytes
csrr t1, vl # Get number of bytes fetched
vmseq.vi v0, v4, 0 # Flag zero bytes
vmfirst.m a3, v0 # Zero found?
vmsif.m v0, v0 # Set mask up to and including zero byte.
add a1, a1, t1 # Bump pointer
vsb.v v4, (a2), v0.t # Write out bytes
add a2, a2, t1 # Bump pointer
bltz a3, 1b # Zero byte not found, so loop
ret

This relatively simple code (not as simple as memcpy, obviously) copies 64 bytes (512 bits) in each loop iteration on this chip that has 128 bit vector registers, used in groups of 4 (the m4 in the vsetvli). It correctly handles all the problems:

- unaligned src or dst works fine, and doesn't significantly affect the speed

- if the vlbuff.v load instruction attempts to read into a memory page you don't have access rights to, it automatically shortens the vector length to the number of bytes it could actually read. vlbuff.v only causes an exception if the first byte can not be read (the ff means "Fault on First")

- the vsb.v store instruction uses a mask v0.t to ensure it doesn't disturb any bytes past where the terminating null is written. It will correctly copy a string into the middle of existing data.

On the Allwinner D1 (a low end SoC being marketed against ARM Cortex A7 or A35) this strcpy code runs at 43.75 clock cycles per 64 bytes copied.

That's 10.24x faster than the example code presented in this article, 7.3x than my improved version (matching the C code), and 2.2x faster than the current (non-vector) glibc code.

That's pretty good, especially considering that the code is barely more complex than the naive C byte-at-a-time loop.

Benchmark results on the Allwinner D1, and the glibc code can be found here: http://hoult.org/d1_strcpy.txt

And the same for memcpy here: http://hoult.org/d1_memcpy.txt

ARM SVE should allow fairly similar code, but I believe general consumer availability of chips with SVE is probably a year or more away still.