Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ harness = false
name = "arraystring"
harness = false

[[bench]]
name = "clone"
harness = false

[features]
default = ["std"]
std = []
Expand Down
43 changes: 43 additions & 0 deletions benches/clone.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@

extern crate arrayvec;
#[macro_use] extern crate bencher;

use arrayvec::ArrayVec;

use bencher::Bencher;
use bencher::black_box;

fn clone_x<const N: usize>(b: &mut Bencher) {
let mut v = ArrayVec::<u8, N>::new();
v.extend((0..N).map(|x| x as u8));
b.iter(|| {
black_box(v.clone())
});
b.bytes = v.len() as u64;
}

fn clone_8(b: &mut Bencher) {
clone_x::<8>(b);
}
fn clone_16(b: &mut Bencher) {
clone_x::<16>(b);
}
fn clone_32(b: &mut Bencher) {
clone_x::<32>(b);
}
fn clone_64(b: &mut Bencher) {
clone_x::<64>(b);
}
fn clone_512(b: &mut Bencher) {
clone_x::<512>(b);
}

benchmark_group!(benches,
clone_8,
clone_16,
clone_32,
clone_64,
clone_512
);

benchmark_main!(benches);
42 changes: 41 additions & 1 deletion src/arrayvec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1193,7 +1193,47 @@ impl<T, const CAP: usize> Clone for ArrayVec<T, CAP>
where T: Clone
{
fn clone(&self) -> Self {
self.iter().cloned().collect()
let mut array = Self::new();

for i in 0..CAP {
if i < self.len() {
// Safety: known to not exceed capacity
unsafe {
array.push_unchecked(self[i].clone());
}
} else {
// we are done copying all the elements.
// we continue to copy uninitialized elements past len up to CAP. This seems
// weird and counter intuitive, but when T is trivial (like i8/i32), copying the
// whole array is faster as the compiler doesnt have to loop up to len, and the
// generated code is branchless copy of the whole struct (like Copy).
//
// We only do it if:
// - T does not require drop, which we take as an indication that T::clone()
// is not trivial and therefore the loop will not be optimized away by the
// compiler.
// - size_of > 0, to avoid reading self.xs[i].as_ptr()
// - the total array is less or equal to 128 bytes. An arbitrary threshold to
// avoid unnecessary copying of large arrays.
if mem::needs_drop::<T>()
|| mem::size_of::<T>() == 0
|| CAP > 128 / mem::size_of::<T>()
{
break;
}

// Safety: copy as MaybeUninit<T>
unsafe {
ptr::copy_nonoverlapping(&self.xs[i], &mut array.xs[i], 1);
}
};
}

// This assignment helps the compiler understand it can just copy the len instead
// of using the accumulated value.
// This is especially useful for T::clone() that can not panic.
array.len = self.len;
array
}

fn clone_from(&mut self, rhs: &Self) {
Expand Down
8 changes: 8 additions & 0 deletions tests/codegen/clone_copy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
extern crate arrayvec;
use arrayvec::ArrayVec;

#[unsafe(no_mangle)]
#[inline(never)]
pub fn test_subject(array: &ArrayVec<u8, 8>) -> ArrayVec<u8, 8> {
array.clone()
}
51 changes: 51 additions & 0 deletions tests/codegen/justfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
root := "../.."

default: check

build-deps:
cd {{root}} && cargo build --release

# Compile and inspect the asm for .clone() testcase
# Require that it's very short (should be like a short struct copy)
check: build-deps
#!/usr/bin/env bash
set -euo pipefail

ROOT={{root}}
asm=$(rustc --edition 2024 -C opt-level=3 --crate-type lib \
clone_copy.rs -L "$ROOT/target/release/deps" \
--emit=asm -o -)

fail=

FUNC=test_subject
MAXBODY=20

# Sanity check
echo "$asm" | grep -q "$FUNC.*:" || { echo "FAIL: $FUNC label missing"; fail=1; }

# Assertions
echo "$asm" | grep -q -E 'memcpy|memmove' && { echo "FAIL: references memcpy"; fail=1; }
echo "$asm" | grep -q -E 'panic' && { echo "FAIL: references panic"; fail=1; }

# Extract the function body
body=$(echo "$asm" | sed -n "/$FUNC.*:$/,/\.cfi_endproc/p")
nlines=$(echo "$body" | wc -l | tr -d ' ')
if [ "$nlines" -gt "$MAXBODY" ]; then
echo "FAIL: body too long"
fail=1
fi


if [ "${fail:-0}" = 1 ]; then
echo "--- output (full) ---"
echo "$asm"
echo "--- end ---"
exit 1
else
echo "--- output (body) ---"
echo "$body"
echo "--- end ---"
fi

echo "PASS: function $FUNC"
21 changes: 21 additions & 0 deletions tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,27 @@ fn test_simple() {
assert_eq!(sum_len, 8);
}

#[test]
fn test_clone_simple() {
let data = [1, 2, 3, 4, 5, 6, 7];
let mut vec = ArrayVec::from(data);
vec.pop();

let u = vec.clone();
assert_eq!(vec, u);
assert_eq!(&vec, &data[..data.len() - 1]);
}

#[test]
fn test_clone_complex() {
let mut vec: ArrayVec<Vec<i32>, 3> = ArrayVec::new();

vec.push(vec![1, 2, 3, 4]);
vec.push(vec![10]);
let u = vec.clone();
assert_eq!(vec, u);
}

#[test]
fn test_capacity_left() {
let mut vec: ArrayVec<usize, 4> = ArrayVec::new();
Expand Down
Loading