Optimize str_repeat - #23128
Optimize str_repeat#23128divinity76 wants to merge 2 commits into
Conversation
The memmove got me thinking "can src and dst actually overlap?", and it turns out no: - e always points to populated data + 1 - l may reach the end of populated data, but will never exceed it memcpy is safe here, and faster (unless the compiler already spotted it in an optimization pass and replaced it internally, entirely possible)
There was a problem hiding this comment.
Pull request overview
This pull request optimizes PHP_FUNCTION(str_repeat) in ext/standard/string.c by replacing a memmove() with memcpy() in the exponential self-copy loop used to build the repeated string, based on the non-overlap guarantee of the source and destination ranges in that loop.
Changes:
- Replace
memmove(e, s, l)withmemcpy(e, s, l)in the loop that expands the already-copied prefix into the remaining buffer.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
Uh, from my knowledge the only difference between them is memmove check if s and e is the same, and is slower than memcpy.
In this case, I can't imagine any edge-cases because the two address shouldn't be the same. So this is sensible. But I don't know whether we want minor optimizations, because compilers are smart, and I don't sure if there is more difference between them.
David is knowledgeable about UNIX stuff. Maybe he could decide this. cc @devnexen :)
LamentXU123
left a comment
There was a problem hiding this comment.
From my perspective. But please wait for other's opinion.
|
difference between them.
It s nothing unix, just C :) |
|
@divinity76 the change is not wrong but I sense the "optimisation" part might not be remarkable across the board, depending on libc and platforms. Did you benchmark it a bit ? To summarize: it is probably good to go, but a shift in the wording might be asked. |
|
I assume people familiar with unix is also familiar with this as well (^_^)
I am sure some compilers are smart enough to do this already in this case. |
No, In all likelihood It's only saving a couple of asm instructions to the tune of ; check if memcpy is safe
mov eax,src
add eax,len
cmp eax, dst
jl memcpy_is_safe
- 5205fc: e8 2f 1f ce ff call 202530 <memcpy@plt>
+ 5205fc: e8 cf 18 ce ff call 201ed0 <memmove@plt>
Suggestions? Would you prefer |
|
x86-64 (glibc) builds both from the same file, strong_alias (MEMMOVE_SYMBOL (__memmove, unaligned), MEMCPY_SYMBOL (__memcpy, unaligned)), so memcpy is the very same address, nothing saved. aarch64 shares the small-copy code up to 128 bytes, no overlap test at all, and beyond that memmove does sub / cbz / cmp / b.hs L(copy_long) into memcpy s own long path, so 4 instructions. musl just forwards, if ((uintptr_t)s-(uintptr_t)d-n <= -2*n) return memcpy(d, s, n);. And where memmove is a plainer loop instead, it is not a fixed branch anymore, it grows with l. And I m just talking about Linux here.. this is what I was trying to say with |
iliaal
left a comment
There was a problem hiding this comment.
Looks fine to me, although I doubt there is any performance to be gained here, more of a correctness refactor I'd say
Lets compare the glibc 2.42 implementation of memmove: rettype
inhibit_loop_to_libcall
MEMMOVE (a1const void *a1, a2const void *a2, size_t len)
{
unsigned long int dstp = (long int) dest;
unsigned long int srcp = (long int) src;
/* This test makes the forward copying code be used whenever possible.
Reduces the working set. */
if (dstp - srcp >= len) /* *Unsigned* compare! */
{
/* Copy from the beginning to the end. */
#if MEMCPY_OK_FOR_FWD_MEMMOVE
dest = memcpy (dest, src, len);
#else
/* If there not too few bytes to copy, use word copy. */
if (len >= OP_T_THRES)
{with it's memcpy counterpart: void *
MEMCPY (void *dstpp, const void *srcpp, size_t len)
{
unsigned long int dstp = (long int) dstpp;
unsigned long int srcp = (long int) srcpp;
/* Copy from the beginning to the end. */
/* If there not too few bytes to copy, use word copy. */
if (len >= OP_T_THRES)
{Memcpy saves the CPU from having to do the |
Actullay, modern x86-64 glibc doesn't actually build them into libc for this code path. The implementation that runs on x86-64 is sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S, I don't think this change should be called an "Optimization" :) it's really tiny — anyway, the change itself is safe. I have only one suggestion: add a comment pinning the non-overlap invariant, in case of somebody in the future change this behavior, then silently turn this into UB :): |
Neat, so, as pointed out by both devnexen and laruence , there is no change on x86-64 glibc. But in ARM64/aarch64 glibc: https://github.com/bminor/glibc/blob/master/sysdeps/aarch64/memcpy.S in memmove ENTRY (MEMMOVE)
(...)
L(move_long):
/* Only use backward copy if there is an overlap. */
sub tmp1, dstin, src
cbz tmp1, L(move0)
cmp tmp1, count
b.hs L(copy_long)
sub tmp1, dstin, src
cbz tmp1, L(move0)
cmp tmp1, count
b.hs L(copy_long)on arm64/aarch64 glibc.
hmhmhm...
agreed.
That sounds good. Not important, but can you go to https://github.com/php/php-src/pull/23128/changes and add a comment on the memcpy line including the content |
|
|
||
| while (e<ee) { | ||
| l = (e-s) < (ee-e) ? (e-s) : (ee-e); | ||
| memmove(e, s, l); |
There was a problem hiding this comment.
can you try using ```suggestion
like the codeblock language is "suggestion" ?
|
|
||
| while (e<ee) { | ||
| l = (e-s) < (ee-e) ? (e-s) : (ee-e); | ||
| memmove(e, s, l); |
There was a problem hiding this comment.
very close! the last line got 3 indentation levels too many
Co-authored-by: Xinchen Hui <laruence@gmail.com>
The memmove got me thinking "can src and dst actually overlap?", and it turns out no:
memcpy is safe here, and faster (unless the compiler already spotted it in an optimization pass and replaced it internally, entirely possible)