Skip to content

Optimize ArrayVec::clone() to behave like Copy when T: Copy#315

Merged
bluss merged 5 commits into
bluss:masterfrom
barakugav:copy-past-len
Jul 9, 2026
Merged

Optimize ArrayVec::clone() to behave like Copy when T: Copy#315
bluss merged 5 commits into
bluss:masterfrom
barakugav:copy-past-len

Conversation

@barakugav

@barakugav barakugav commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

First of all, love the library! use it all the time 😄

This PR optimize ArrayVec::clone() by copying elements past len up to CAP. This sounds weird and counter intuitive, but for small arrays of trivial types this optimizations remove bound checks and compiles to very simple assembly.

It also include a small benchmark for ArrayVec::clone() that demonstrate the speed up.

# before
> cargo bench clone
running 5 tests
test clone_16  ... bench:           7 ns/iter (+/- 0) = 2285 MB/s
test clone_32  ... bench:          22 ns/iter (+/- 2) = 1454 MB/s
test clone_512 ... bench:         327 ns/iter (+/- 14) = 1565 MB/s
test clone_64  ... bench:          33 ns/iter (+/- 4) = 1939 MB/s
test clone_8   ... bench:           4 ns/iter (+/- 0) = 2000 MB/s

# after
> cargo bench clone
running 5 tests
test clone_16  ... bench:           3 ns/iter (+/- 0) = 5333 MB/s
test clone_32  ... bench:           8 ns/iter (+/- 0) = 4000 MB/s
test clone_512 ... bench:         199 ns/iter (+/- 15) = 2572 MB/s
test clone_64  ... bench:          24 ns/iter (+/- 3) = 2666 MB/s
test clone_8   ... bench:           1 ns/iter (+/- 1) = 8000 MB/s

As you can see in the code, i bounded the optimization to 128 bytes, but apparently even 512 CAP arrays get a speed up. I didnt check, but my guess is that the asm is simpler without the iterator.

For a simple function like this one:

#[inline(never)]
fn do_work(array: &ArrayVec<u8, 8>) -> ArrayVec<u8, 8> {
    array.clone()
}

This is the assembly on my machine after the PR:

hello::do_work:
Lfunc_begin6:
        .cfi_startproc
        ldr w8, [x1]
        str w8, [x0]
        ldur d0, [x1, #4]
        stur d0, [x0, #4]
        ret

This is the same function before the PR:

hello::do_work:
Lfunc_begin6:
        .cfi_startproc
        sub sp, sp, #96
        .cfi_def_cfa_offset 96
        stp x26, x25, [sp, #16]
        stp x24, x23, [sp, #32]
        stp x22, x21, [sp, #48]
        stp x20, x19, [sp, #64]
        stp x29, x30, [sp, #80]
        add x29, sp, #80
        .cfi_def_cfa w29, 16
        .cfi_offset w30, -8
        .cfi_offset w29, -16
        .cfi_offset w19, -24
        .cfi_offset w20, -32
        .cfi_offset w21, -40
        .cfi_offset w22, -48
        .cfi_offset w23, -56
        .cfi_offset w24, -64
        .cfi_offset w25, -72
        .cfi_offset w26, -80
        mov x19, x0
        ldr w21, [x1]
        cbz w21, LBB6_6
        mov x23, #0
        add x8, x1, x21
        add x22, x8, #4
        add x24, x1, #4
        add x8, sp, #4
        add x25, x8, #4
Lloh6:
        adrp x20, l___unnamed_3@PAGE
Lloh7:
        add x20, x20, l___unnamed_3@PAGEOFF
        b LBB6_3
LBB6_2:
        strb w26, [x25, x23]
        add x23, x23, #1
        cmp x21, x23
        b.eq LBB6_5
LBB6_3:
        ldrb w26, [x24, x23]
        cmp x23, #8
        b.ne LBB6_2
        mov x0, x20
        bl arrayvec::arrayvec::extend_panic
        b LBB6_2
LBB6_5:
        sub w8, w22, w24
        b LBB6_7
LBB6_6:
        mov w8, #0
LBB6_7:
        str w8, [sp, #4]
        ldr w8, [sp, #12]
        str w8, [x19, #8]
        ldur x8, [sp, #4]
        str x8, [x19]
        .cfi_def_cfa wsp, 96
        ldp x29, x30, [sp, #80]
        ldp x20, x19, [sp, #64]
        ldp x22, x21, [sp, #48]
        ldp x24, x23, [sp, #32]
        ldp x26, x25, [sp, #16]
        add sp, sp, #96
        .cfi_def_cfa_offset 0
        .cfi_restore w30
        .cfi_restore w29
        .cfi_restore w19
        .cfi_restore w20
        .cfi_restore w21
        .cfi_restore w22
        .cfi_restore w23
        .cfi_restore w24
        .cfi_restore w25
        .cfi_restore w26
        ret
        .loh AdrpAdd    Lloh6, Lloh7

Thanks for the support in advance ❤️

@barakugav barakugav force-pushed the copy-past-len branch 6 times, most recently from 484c9d7 to 7c25709 Compare July 8, 2026 10:55
@barakugav barakugav changed the title Optimization: copy past len in Clone for simpler asm Optimize ArrayVec::clone() to behave like Copy when T: Copy Jul 8, 2026
@bluss

bluss commented Jul 8, 2026

Copy link
Copy Markdown
Owner

Sounds useful. Duplicating the extend code is not my favourite solution.

I can't reproduce your performance gains right now on this development machine (my arch is an intel tiger lake laptop) so they are probably arch dependent. Please evaluate this improvement as an alternatvie, which might be much simpler for a similar gain, hopefully? I have looked at generated code, and I can see that it's a middle ground, not an equivalent improvement.

diff --git src/arrayvec.rs src/arrayvec.rs
index eec77328..b590cd9d 100644
--- src/arrayvec.rs
+++ src/arrayvec.rs
@@ -1193,7 +1193,12 @@ impl<T, const CAP: usize> Clone for ArrayVec<T, CAP>
     where T: Clone
 {
     fn clone(&self) -> Self {
-        self.iter().cloned().collect()
+        let mut new = ArrayVec::<T, CAP>::new();
+        // Safety: we know it fits the capacity
+        unsafe {
+            new.extend_from_iter::<_, false>(self.iter().cloned());
+        }
+        new
     }

     fn clone_from(&mut self, rhs: &Self) {

@barakugav

barakugav commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

Nice suggestion, it eliminate the checks and the panic paths.
These are my measurements

running 5 tests
test clone_16  ... bench:           4 ns/iter (+/- 2) = 4000 MB/s
test clone_32  ... bench:          10 ns/iter (+/- 1) = 3200 MB/s
test clone_512 ... bench:         201 ns/iter (+/- 38) = 2547 MB/s
test clone_64  ... bench:          24 ns/iter (+/- 3) = 2666 MB/s
test clone_8   ... bench:           2 ns/iter (+/- 0) = 4000 MB/s

Slightly slower for the smaller array. The measurement are very short, but the diff is consistent between runs.

This is the asm:

hello::do_work:
Lfunc_begin6:
        .cfi_startproc
        sub sp, sp, #48
        .cfi_def_cfa_offset 48
        stp x20, x19, [sp, #16]
        stp x29, x30, [sp, #32]
        add x29, sp, #32
        .cfi_def_cfa w29, 16
        .cfi_offset w30, -8
        .cfi_offset w29, -16
        .cfi_offset w19, -24
        .cfi_offset w20, -32
        mov x19, x0
        ldr w20, [x1]
        cbz w20, LBB6_2
        add x0, sp, #8
        add x1, x1, #4
        mov x2, x20
        bl _memcpy
        ldr x8, [sp, #8]
        b LBB6_3
LBB6_2:
LBB6_3:
        str w20, [x19]
        stur x8, [x19, #4]
        .cfi_def_cfa wsp, 48
        ldp x29, x30, [sp, #32]
        ldp x20, x19, [sp, #16]
        add sp, sp, #48
        .cfi_def_cfa_offset 0
        .cfi_restore w30
        .cfi_restore w29
        .cfi_restore w19
        .cfi_restore w20
        ret

Another solution that produce a very similar assembly and comparable measurements is adding hint::assert_unchecked(len < CAP), like i did in #314.
With it, there is no need to change Clone, at least to match your suggestion.
You might want to consider it instead, as it optimize many more functions that touch ArrayVec.

Its hard for me to give up on the branchless, Copy-like asm. Do you think there is a way to edit it to be more reasonable and accepted? The main requirement is to copy the uninit data from len to CAP, so the compiler can just ignore len for Copy types.

@barakugav

barakugav commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

I use godbolt to see the asm created on x64, huge difference

This PR

https://godbolt.org/z/eW3q54cxx

example::do_work::ha9fd69377e196564:
        mov     rax, rdi
        movzx   ecx, word ptr [rsi]
        mov     word ptr [rdi], cx
        mov     rcx, qword ptr [rsi + 2]
        mov     qword ptr [rdi + 2], rcx
        ret

CHECK=false

https://godbolt.org/z/8T6qoh6nq

example::do_work::ha9fd69377e196564:
        push    r14
        push    rbx
        push    rax
        mov     rbx, rdi
        movzx   r14d, word ptr [rsi]
        test    r14, r14
        je      .LBB1_1
        add     rsi, 2
        mov     rdi, rsp
        mov     rdx, r14
        call    qword ptr [rip + memcpy@GOTPCREL]       # <-- call to memcpy
        mov     rax, qword ptr [rsp]
        jmp     .LBB1_3
.LBB1_1:
.LBB1_3:
        mov     word ptr [rbx], r14w
        mov     qword ptr [rbx + 2], rax
        mov     rax, rbx
        add     rsp, 8
        pop     rbx
        pop     r14
        ret

Original code

https://godbolt.org/z/5h7cqneeE

example::do_work::ha9fd69377e196564:
        push    rbx
        mov     rax, rdi
        movzx   ebx, word ptr [rsi]
        test    rbx, rbx
        je      .LBB1_1
        add     rbx, 2
        movzx   ecx, byte ptr [rsi + 2]
        cmp     ebx, 3
        jne     .LBB1_5
        mov     si, 1
        jmp     .LBB1_19
.LBB1_1:
        xor     esi, esi
        jmp     .LBB1_19
.LBB1_5:
        movzx   edx, byte ptr [rsi + 3]
        cmp     ebx, 4
        jne     .LBB1_7
        mov     si, 2
        jmp     .LBB1_19
.LBB1_7:
        movzx   edi, byte ptr [rsi + 4]
        cmp     ebx, 5
        jne     .LBB1_9
        mov     si, 3
        jmp     .LBB1_19
.LBB1_9:
        movzx   r8d, byte ptr [rsi + 5]
        cmp     ebx, 6
        jne     .LBB1_11
        mov     si, 4
        jmp     .LBB1_19
.LBB1_11:
        movzx   r9d, byte ptr [rsi + 6]
        cmp     ebx, 7
        jne     .LBB1_13
        mov     si, 5
        jmp     .LBB1_19
.LBB1_13:
        movzx   r10d, byte ptr [rsi + 7]
        cmp     ebx, 8
        jne     .LBB1_15
        mov     si, 6
        jmp     .LBB1_19
.LBB1_15:
        movzx   r11d, byte ptr [rsi + 8]
        cmp     ebx, 9
        jne     .LBB1_17
        mov     si, 7
        jmp     .LBB1_19
.LBB1_17:
        cmp     ebx, 10
        jne     .LBB1_4
        movzx   ebx, byte ptr [rsi + 9]
        mov     si, 8
.LBB1_19:
        mov     word ptr [rax], si
        mov     byte ptr [rax + 2], cl
        mov     byte ptr [rax + 3], dl
        mov     byte ptr [rax + 4], dil
        mov     byte ptr [rax + 5], r8b
        mov     byte ptr [rax + 6], r9b
        mov     byte ptr [rax + 7], r10b
        mov     byte ptr [rax + 8], r11b
        mov     byte ptr [rax + 9], bl
        pop     rbx
        ret
.LBB1_4:
        lea     rdi, [rip + .Lanon.466b45355eb502565aec0b178a18bdf9.1]
        call    qword ptr [rip + example::extend_panic::ha5d83a66ac79d21b@GOTPCREL]

@bluss

bluss commented Jul 9, 2026

Copy link
Copy Markdown
Owner

I maintainer-pushed an addition of commits which I think makes the changes reasonable/palatable. Part of the solution is cleaning up extend so that it's not so sad to look at. There is a risk of losing extend optimizations - because the old benchmarks were not really functional (they were testing something that was optimized out - and some of them still may be).

I simplified the clone implementation, removed everything possible while preserving the "simple struct copy" as a benchmark.

From you, but here it is in local form, using rustc.
Ideally we should have a test that verifies this - or just keep the file around if it's not automatic.

extern crate arrayvec;
use arrayvec::ArrayVec;

#[inline(never)]
pub fn do_work(array: &ArrayVec<u8, 8>) -> ArrayVec<u8, 8> {
    array.clone()
}
rustc --edition 2021 -C opt-level=3 --crate-type lib testfun.rs -L target/release/deps --emit=asm -o -

Output:

ZN7testfun7do_work17h27b711b5dad6bbccE:
        .cfi_startproc
        movq    %rdi, %rax
        movl    (%rsi), %ecx
        movl    %ecx, (%rdi)
        movq    4(%rsi), %rcx
        movq    %rcx, 4(%rdi)
        retq

@bluss

bluss commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Let me know if this simplification of the clone optimization still preserves performance in your tests/your eyes.

Comment thread src/arrayvec.rs Outdated

// Safety: copy of MaybeUninit to MaybeUninit
unsafe {
ptr::copy_nonoverlapping(self.xs[i].as_ptr(), array.xs[i].as_mut_ptr(), 1)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
ptr::copy_nonoverlapping(self.xs[i].as_ptr(), array.xs[i].as_mut_ptr(), 1)
ptr::copy_nonoverlapping(&self.xs[i], &mut array.xs[i], 1)

Not sure about the exact semantics of UB, but instead of copy_nonoverlapping::<T>(..) we should probably use copy_nonoverlapping::<MaybeUninit<T>>(..)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, thank you, I also was reviewing this and agree.

@barakugav

Copy link
Copy Markdown
Contributor Author

both code and asm looks great, thanks!

wrote a small comment, want me to fix it?

wdyt about #314?

@bluss

bluss commented Jul 9, 2026

Copy link
Copy Markdown
Owner

I fixed that mistake, thanks. I also back-edited one of your changes so that the bench uses std::hint::black_box. Added in the codegen test I used for editing the changes (it's not in CI yet).

The clone code is now in fact even simpler (and it could be simpler, literally array.push(self[i].clone()) works); how to pick which level of detail to use I don't know -- using push_unchecked seems motivated, at least.

@bluss

bluss commented Jul 9, 2026

Copy link
Copy Markdown
Owner

I'm sorry for the back and forth, but now that clone and extend are untangled, the extend changes will be in PR #316, which should feel less weird for the contributor who got stuck with extra changes in their PR.

barakugav and others added 5 commits July 9, 2026 22:39
Desired feature is that clone of ArrayVec<u8, 8> should look like a
simple struct copy (regardless of how full the arrayvec is).

Simplify the clone implementation while preserving this.

Scope guard for length not found to be necessary. Write element and
length possible but it is equivalent to push_unchecked which also worked
here.
@bluss

bluss commented Jul 9, 2026

Copy link
Copy Markdown
Owner

I also back-edited one of your changes so that the bench uses std::hint::black_box

I reverted this (back to bencher::black_box). Mainly because it changes the benchmark so much that I don't see the difference; but I'm not sure I see the difference in the benchmark with the revert either - safer to leave it as you wrote it, feel free to revisit that or not (codegen test was clearly easier to use as goal here).

Thanks for a nice optimization

@bluss bluss merged commit b0ff9cd into bluss:master Jul 9, 2026
7 checks passed
@barakugav barakugav deleted the copy-past-len branch July 9, 2026 21:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants