[PATCH v2] aarch64: add optimised strspn/strcspn
Wilco Dijkstra
Wilco.Dijkstra@arm.com
Tue Aug 19 19:08:49 GMT 2025
Hi remph,
> Wilco: thanks for the feedback; here's an implementation with a bit array
> and 2-register TBL. This version is much smaller, since there should be
> no need for different loops if 2-register TBL is generally fast enough,
> which means a faster init time. Speed is comparable to the best case of
> my previous version, and there is no worst case.
The new version is seriously fast indeed! For longer strings it is now ~5x faster,
and even short strings are always faster (they were slower previously).
I have a few comments below.
Also would you mind posting a version to Arm Optimized Routines [1]?
We maintain AArch64 optimized string functions there under a permissive license
so they can be used in other projects and libraries. The assembly code is identical
apart for the license, include filename and any libc_hidden_def macros.
[1] https://github.com/ARM-software/optimized-routines
Cheers,
Wilco
+ENTRY(STRSPN)
+ ldrb w2, [set]
+ cbz w2, L(early)
+#ifdef USE_AS_STRCSPN
+ ldrb w3, [set, 1]
+ cbz w3, L(early)
+#endif
+ /* Table has ones for bytes to reject and zeros for bytes to accept */
+#ifdef USE_AS_STRCSPN
+ stp xzr, xzr, [sp, -16]
+ stp xzr, xzr, [sp, -32]!
Missing unwind directive (and below).
+#else
+ mvni v0.4s, 0
+ stp q0, q0, [sp, -32]!
+#endif
+
+ mov one, 1
Why not move this before the stp so that you can use it in the 2nd STP and
ensure the bit for NUL char is set already? Then the defines below can be
removed and we avoid 1 iteration for USE_AS_STRCSPN.
+ .balign 32,,16
+L(fill_table):
+#ifdef USE_AS_STRCSPN
+ ldrb w2, [set], 1
+#endif
+ lsr byte_i, x2, 6 /* x2 / 64 */
+ lsl bits_i, one, x2 /* x2 % 64 implicitly */
+ ldr x5, [sp, byte_i, lsl 3]
+ SBT x5, x5, bits_i
+ str x5, [sp, byte_i, lsl 3]
+#ifndef USE_AS_STRCSPN
+ ldrb w2, [set, 1]!
+#endif
+ cbnz w2, L(fill_table)
+
+ ld1 {table}, [sp], 32
Why not use .2d for the table? That avoids the need to do extra work
for big-endian since the loop above deals with 64-bit words already.
+ ubfiz off, og_s, 2, 4 /* Bottom 4 bits, times 4 to count nibbles */
+ and s, og_s, -16 /* Round S down to 16-byte boundary */
+ movi sevens, 7
+#ifdef __AARCH64EB__
+ rev64 table_a, table_a
+ rev64 table_b, table_b
+#endif
+
+ .balign 64,,16
This doesn't seem to be very effective - this loop is at a bad alignment.
We typically use .p2align 4 for performance critical loops. If you use the max
padding feature, you always need a 2nd directive for minimum alignment
(so you avoid getting no alignment at all if the padding would be too large).
+L(loop):
+ ldr q0, [s], 16
+ mov save_off, off /* OFF and SYNDROME overlap */
This is highly confusing...
+ ushr vbyte_i, v0.16b, 3
+ bic vbits_i, sevens, v0.16b
+ tbl v0.16b, {table}, vbyte_i
+ /* Bring the relevant bit to the MSB of each byte */
+ sshl v0.16b, v0.16b, vbits_i
+ /* Set every bit of each byte to its MSB */
+ cmlt v0.16b, v0.16b, 0
+ /* Bytes->nibbles */
+ shrn v0.8b, v0.8h, 4
+ fmov x2, d0
+ LS_FW syndrome, x2, syndrome
And also relies on this implicitly setting save_off to zero in the next iteration...
+ cbz syndrome, L(loop)
+
+#ifndef __AARCH64EB__
+ rbit syndrome, syndrome
+#endif
+ sub s, s, 16
+ sub x0, s, og_s
+ clz syndrome, syndrome
+ add x0, x0, save_off, lsr 2
+ add x0, x0, syndrome, lsr 2
+ ret
The extra save_off addition here adds another 2 cycles of latency.
I think this could be done by using a mask to clear off bits from syndrome
rather than a variable shift, and then setting the mask to zero for the next
iteration. Then you don't ever need the extra ADD at the end.
Or even better, treat the first 16 bytes specially - then the shift works fine
and you just return ctz (syndrome) >> 2. Plus it allows interleaving of the
instructions, eg. moving the ldr q0, [s] much earlier. The first 16 bytes will
be faster as you save at least 6 instructions, we remove 2 instructions from
the loop and 2 in the tail.
+L(early):
+#ifdef USE_AS_STRCSPN
+ /* strlen(set) < 2: call strchrnul(s, *set) and get its offset from S */
+ stp fp, lr, [sp, -64]!
+ str x19, [sp, 32]
+ mov w1, w2
+ mov fp, sp
+ mov x19, x0
+ bl strchrnul
This should be __strchrnul to avoid localplt failures in GLIBC testsuite...
+ sub x0, x0, x19
+ ldr x19, [sp, 32]
+ ldp fp, lr, [sp], 64
This also needs unwind directives (easiest option is to copy them from C).
More information about the Libc-alpha
mailing list