[EXT] [PATCH 1/1] riscv: Add Zbkb optimized repeat_bytes helper
Pincheng Wang
pincheng.plct@isrc.iscas.ac.cn
Mon Sep 15 12:40:42 GMT 2025
在 2025/9/12 1:21, Pincheng Wang 写道:
> On 2025/9/12 1:03, Peter Bergner wrote:
>> On 9/11/25 10:48 AM, Pincheng Wang wrote:
>>> +/* Miscellaneous functions used in string implementations. Generic
>>> C version.
>>> + Copyright (C) 2023-2025 Free Software Foundation, Inc.
>>> + This file is part of the GNU C Library.
>>
>> Others can correct me if I'm wrong, but since this is a new file, I think
>> you should be using "2025" for the copyright year, rather than
>> "2023-2025".
>>
>>
>
> You're right, this is a new file. I copied the header from sysdeps/
> generic/string-misc.h and missed adjusting the years. I'll change it to
> the right form and submit a V2 patch.
>
>>
>>
>>> +/* Setup an word with each byte being c_in. For instance, on a 64 bits
>>> + machine with input as 0xce the functions returns
>>> 0xcececececececece. */
>>> +static __always_inline op_t
>>> +repeat_bytes (unsigned char c_in)
>>> +{
>>> + op_t c = c_in;
>>> +#if defined __riscv_zbkb && __riscv_xlen == 64
>>> + /* 8bit -> 16bit -> 32bit -> 64bit pattern replication */
>>> + __asm__ ("packh %0, %1, %1\n\t"
>>> + "packw %0, %0, %0\n\t"
>>> + "pack %0, %0, %0"
>>> + : "=r"(c) : "r"(c));
>>> +#elif defined __riscv_zbkb && __riscv_xlen == 32
>>> + /* 8bit -> 16bit -> 32bit pattern replication */
>>> + __asm__ ("packh %0, %1, %1\n\t"
>>> + "pack %0, %0, %0"
>>> + : "=r"(c) : "r"(c));
>>> +#else
>>> + c = ((op_t)-1 / 0xff) * c_in;
>>> +#endif
>>> + return c;
>>> +}
>>
>> It seems this inline asm creates a lot of unneeded data dependencies,
>> which may affect low end cores??? Maybe rewrite it to remove those using
>> some temp variables and a separate output variable?
>>
>> ...or maybe people don't think we should care?
>>
>> Peter
>>
>
> Good point. My current sequence does reuse the same register and create
> a dependency chain. For low-end/short pipeline cores that can matter. In
> V2 I'll rewrite this inline asm, use separate temporaries and a distinct
> output to reduce false deps and give the scheduler freedom. Below is one
> possible approach for reference:
>
> RV64:
> static __always_inline op_t
> repeat_bytes (unsigned char c_in)
> {
> op_t out, t16, t32;
> #if defined __riscv_zbkb && __riscv_xlen == 64
> __asm__ (
> "packh %1, %3, %3\n\t" /* t16 = replicate 8b -> 16b */
> "packw %2, %1, %1\n\t" /* t32 = replicate 16b -> 32b */
> "pack %0, %2, %2" /* out = replicate 32b -> 64b */
> : "=&r"(out), "=&r"(t16), "=&r"(t32)
> : "r"((op_t)c_in)
> );
> return out;
> #else
> return ((op_t)-1 / 0xff) * c_in;
> #endif
> }
>
> RV32:
> static __always_inline op_t
> repeat_bytes (unsigned char c_in)
> {
> op_t out, t16;
> #if defined __riscv_zbkb && __riscv_xlen == 32
> __asm__ (
> "packh %1, %2, %2\n\t" /* t16 = replicate 8b -> 16b */
> "pack %0, %1, %1" /* out = replicate 16b -> 32b */
> : "=&r"(out), "=&r"(t16)
> : "r"((op_t)c_in)
> );
> return out;
> #else
> return ((op_t)-1 / 0xff) * c_in;
> #endif
> }
>
> I'll benchmark both versions (current single-reg vs. temp-based) with
> benchtests; I'm also trying to get runs on real hardware in addition to
> QEMU to confirm there's no regression on simpler cores.
>
> Thanks again for the helpful suggestions!
>
> Best,
> Wang
Hi Peter,
Thanks again for your feedback!
I recently ran some additional performance tests for this patch on real
hardware to better understand the potential impact of data dependencies.
Specifically, I compared three version of `repeat_bytes`:
op_t origin_repeat_bytes(unsigned char c_in) {
return ((op_t)-1 / 0xff) * c_in);
}
op_t zbkb_v1_repeat_bytes(unsigned char c_in) {
op_t c = c_in;
__asm__ volatile (
"packh %0, %1, %1\n\t"
"pack %0, %0, %0"
: "=r"(c) : "r"(c));
return c;
}
op_t zbkb_v2_repeat_bytes(unsigned char c_in) {
op_t out, t16;
__asm__ volatile (
"packh %0, %2, %2\n\t"
"pack %1, %0, %0"
: "=&r"(t16), "=&r"(out)
: "r"((op_t)c_in));
return out;
}
The test were performed on a Raspberry Pi Pico 2, which uses a Hazard 3
three-stage RV32IMACZb* core-likely representative of the "low-end,
short pipeline" cores you mentioned. Each function was executed
100,000,000 times, which execution time measured via hardware timer:
- Origin version: 12,001,363 µs (100%)
- Data-dependent version: 10,666,671 µs (88.88%)
- Dependency-free version: 10,000,003 µs (83.32%)
For completeness, here is the compiler-generated assembly for each version:
1000012e <origin_repeat_bytes>:
1000012e: 00851793 slli a5,a0,0x8
10000132: 953e add a0,a0,a5
10000134: 01051793 slli a5,a0,0x10
10000138: 953e add a0,a0,a5
1000013a: 8082 ret
1000013c <zbkb_v1_repeat_bytes>:
1000013c: 08a57533 packh a0,a0,a0
10000140: 08a54533 pack a0,a0,a0
10000144: 8082 ret
10000146 <zbkb_v2_repeat_bytes>:
10000146: 87aa mv a5,a0
10000148: 08f7f733 packh a4,a5,a5
1000014c: 08e74533 pack a0,a4,a4
10000150: 8082 ret
The "dependency-free" version actually has more instructions and
register usage. My current guess is taht for such a simple chain of
dependencies, the pipeline's forwarding paths are already effective, so
software-side elimination of the dependency may not provide significant
benefit, and could slightly increase pressure on register allocation or so.
Unfortunately, I do not currently have RISC-V hardware suitable for
running a full glibc build, but I think this focused test still provides
useful insight. I would appreciate futher input from you and the
community on which implementation would be preferable.
Thanks again for your helpful suggestions!
Regards,
Pincheng Wang
More information about the Libc-alpha
mailing list