From 7f06cadfd66ab943c844f60eab3d0f338b69e237 Mon Sep 17 00:00:00 2001 From: Justin Hammond Date: Sat, 15 Aug 2026 14:50:58 +0800 Subject: [PATCH 1/2] libs/libc/machine/risc-v: Align strlcpy's destination too. The word loop walks src to a register boundary and then stores a whole register at a time to dst, but nothing establishes that dst is on a boundary too. Where the two pointers disagree about where a boundary falls, every store in that loop is misaligned. The base ISA does not require misaligned stores to be supported. Where firmware emulates them each store traps into machine mode, and where nothing emulates them the store faults, so this is not only a question of speed. Measured on a 1.4 GHz rv64 that emulates them, with a 32 KB string whose src and dst are misaligned by different amounts: generic C 410.4 MB/s this file 7.5 MB/s which is around 178 cycles per byte, flat from 512 bytes to 32 KB. Test the two pointers against each other before going wide, as arch_strcpy.S already does. Pointers that agree still reach the word loop, since walking src to a boundary walks dst to one as well; pointers that disagree take the byte path, where no single boundary serves both. After the change the misaligned case runs at 490 MB/s and the aligned rates are unchanged. The measurements come from the benchmark in apache/nuttx-apps#3706. Assisted-by: Claude:claude-opus-5 Signed-off-by: Justin Hammond --- libs/libc/machine/risc-v/arch_strlcpy.S | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/libs/libc/machine/risc-v/arch_strlcpy.S b/libs/libc/machine/risc-v/arch_strlcpy.S index 6d18cf40e731d..ac90a89dae50a 100644 --- a/libs/libc/machine/risc-v/arch_strlcpy.S +++ b/libs/libc/machine/risc-v/arch_strlcpy.S @@ -72,7 +72,17 @@ ARCH_LIBCFUN(strlcpy): addi a2, a2, -1 /* reserve space for null terminator */ - /* Bytewise copy head: align src to SZREG boundary */ + /* The word loop below aligns src and then stores a register at a + * time to dst, so it is safe only where the two pointers agree about + * where a boundary falls. Where they do not, no single boundary + * serves both and the copy goes a byte at a time. + */ + + xor t0, a0, a1 + andi t0, t0, SZREG-1 + bnez t0, .Lcopy_tail + + /* Bytewise copy head: align src, and with it dst */ .Lcopy_head: beqz a2, .Ltruncated From 364059b435b9e2578fd7763ae211686360fd46e1 Mon Sep 17 00:00:00 2001 From: Justin Hammond Date: Sat, 15 Aug 2026 15:51:58 +0800 Subject: [PATCH 2/2] libs/libc/machine/risc-v: Compare a register at a time on equal offsets. memcmp, strncmp and strcmp reach their word loops only when both pointers are already on a register boundary: or t0, a0, a1 andi t0, t0, SZREG-1 That asks more than the loops need. They load from the two pointers at the same boundary, so what matters is that the two agree about where a boundary falls, not that either is already on one. A pair offset by the same amount can be walked up to the boundary a byte at a time and compared a register at a time from there. The union also holds far less often than the difference. For arbitrary pointers on RV64 it is true about one time in 64 against one in eight, and the case it rejects, two strings carved out of the same buffer, is the common one. Test the difference of the pointers, and walk to the boundary first. arch_strcpy.S and arch_memcpy.S already do this. Keeping every access aligned is not only faster here: the base ISA does not require misaligned loads and stores to be supported at all, so a routine in a machine directory cannot assume one will work, whatever it costs. Measured on a 1.4 GHz rv64, source and destination misaligned by one: before after memcmp 32K 34.4 458.0 MB/s strncmp 32K 32.4 253.0 MB/s strcmp 32K 41.0 280.0 MB/s Each of those was the rate of the byte loop the word loop was meant to replace. Pointers that genuinely disagree still take the byte loop, and the aligned rates are unchanged. The measurements come from the benchmark in apache/nuttx-apps#3706. Assisted-by: Claude:claude-opus-5 Signed-off-by: Justin Hammond --- libs/libc/machine/risc-v/arch_memcmp.S | 25 +++++++++++++++-- libs/libc/machine/risc-v/arch_strcmp.S | 37 ++++++++++++++++++++++++- libs/libc/machine/risc-v/arch_strncmp.S | 29 +++++++++++++++++-- 3 files changed, 85 insertions(+), 6 deletions(-) diff --git a/libs/libc/machine/risc-v/arch_memcmp.S b/libs/libc/machine/risc-v/arch_memcmp.S index 3af0a4602ca82..2110c283a134a 100644 --- a/libs/libc/machine/risc-v/arch_memcmp.S +++ b/libs/libc/machine/risc-v/arch_memcmp.S @@ -45,12 +45,33 @@ ARCH_LIBCFUN(memcmp): beqz a2, .Lequal - /* Check if both pointers share alignment */ + /* The loops below load from both pointers at a boundary, which asks + * that the two agree about where a boundary falls, not that either + * is already on one. Test the difference of the pointers, not their + * union. + */ - or t0, a0, a1 + xor t0, a0, a1 andi t0, t0, SZREG-1 bnez t0, .Lbyte_cmp + /* Same offset: walk both up to the boundary a byte at a time, + * stopping early on a difference. + */ + +.Lalign_head: + andi t0, a0, SZREG-1 + beqz t0, .Laligned + lbu t1, 0(a0) + lbu t2, 0(a1) + bne t1, t2, .Ldiff + addi a0, a0, 1 + addi a1, a1, 1 + addi a2, a2, -1 + beqz a2, .Lequal + j .Lalign_head + +.Laligned: li t0, SZREG bltu a2, t0, .Lbyte_cmp diff --git a/libs/libc/machine/risc-v/arch_strcmp.S b/libs/libc/machine/risc-v/arch_strcmp.S index a0bad30f602c5..3ccbcfbc47b02 100644 --- a/libs/libc/machine/risc-v/arch_strcmp.S +++ b/libs/libc/machine/risc-v/arch_strcmp.S @@ -26,11 +26,46 @@ ARCH_LIBCFUN(strcmp): .cfi_sections .debug_frame .cfi_startproc - or a4, a0, a1 li t2, -1 + + /* Two pointers the same distance past a boundary can be compared + * a register at a time once both are walked up to it. Only + * pointers that disagree about where the boundary falls need the + * byte loop, since no single aligned load serves both. Test the + * difference of the pointers, not their union. + */ + + xor a4, a0, a1 and a4, a4, SZREG-1 bnez a4, .Lmisaligned + /* Same offset: walk both up to the boundary a byte at a time, + * stopping early on a difference or a terminator. + */ + + and a4, a0, SZREG-1 + beqz a4, .Laligned +.Lhead: + lbu a2, 0(a0) + lbu a3, 0(a1) + bne a2, a3, .Lheaddiff + addi a0, a0, 1 + addi a1, a1, 1 + beqz a2, .Lheadeq + and a4, a0, SZREG-1 + bnez a4, .Lhead + j .Laligned + +.Lheaddiff: + sub a0, a2, a3 + ret + +.Lheadeq: + li a0, 0 + ret + +.Laligned: + #if SZREG == 4 li a5, 0x7f7f7f7f #else diff --git a/libs/libc/machine/risc-v/arch_strncmp.S b/libs/libc/machine/risc-v/arch_strncmp.S index df03d5fe13f24..52bc2688d03df 100644 --- a/libs/libc/machine/risc-v/arch_strncmp.S +++ b/libs/libc/machine/risc-v/arch_strncmp.S @@ -44,13 +44,36 @@ ARCH_LIBCFUN(strncmp): beqz a2, .Lequal - /* Check alignment consistency */ + /* The word loop below loads from both pointers at a boundary, which + * asks that the two agree about where a boundary falls, not that + * either is already on one. Test the difference of the pointers, + * not their union. + */ - or t0, a0, a1 + xor t0, a0, a1 andi t0, t0, SZREG-1 bnez t0, .Lbyte_loop - /* Both aligned - load masks */ + /* Same offset: walk both up to the boundary a byte at a time, + * stopping early on a difference or a terminator. + */ + +.Lsnc_align_head: + andi t0, a0, SZREG-1 + beqz t0, .Lsnc_aligned + lbu t0, 0(a0) + lbu t1, 0(a1) + bne t0, t1, .Ldiff + beqz t0, .Lequal + addi a0, a0, 1 + addi a1, a1, 1 + addi a2, a2, -1 + beqz a2, .Lequal + j .Lsnc_align_head + +.Lsnc_aligned: + + /* Load masks */ #if SZREG == 8 lla t2, .Lsnc_mask01