From 8085eeb8ebe3481226f918ac91615e4c8a12eb8d Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Fri, 12 Jun 2026 14:37:39 +0300 Subject: [PATCH] builtin: preserve array capacity when deleting elements (#27419) --- doc/docs.md | 7 +++--- vlib/builtin/array.v | 44 ++++++++++++++++----------------- vlib/builtin/array_flags_test.v | 20 +++++++-------- vlib/builtin/array_test.v | 22 ++++++++++++++++- 4 files changed, 57 insertions(+), 36 deletions(-) diff --git a/doc/docs.md b/doc/docs.md index 4748156df..94c192216 100644 --- a/doc/docs.md +++ b/doc/docs.md @@ -1271,10 +1271,11 @@ There are further built-in methods for arrays: * `a.prepend(arr)` inserts elements of array `arr` at the beginning * `a.trim(new_len)` truncates the length (if `new_length < a.len`, otherwise does nothing) * `a.clear()` empties the array without changing `cap` (equivalent to `a.trim(0)`) -* `a.delete_many(start, size)` removes `size` consecutive elements from index `start` - – triggers reallocation +* `a.delete_many(start, size)` removes `size` consecutive elements from index `start`, + preserving order. On the C backend, it reuses the backing buffer and keeps capacity when + no slices share it * `a.delete(index)` equivalent to `a.delete_many(index, 1)` -* `a.delete_last()` removes the last element +* `a.delete_last()` removes the last element; on the C backend it does not change `cap` * `a.first()` equivalent to `a[0]` * `a.last()` equivalent to `a[a.len - 1]` * `a.pop()` removes the last element and returns it diff --git a/vlib/builtin/array.v b/vlib/builtin/array.v index 0ef6d56ea..df267ee39 100644 --- a/vlib/builtin/array.v +++ b/vlib/builtin/array.v @@ -23,8 +23,8 @@ pub: @[flag] pub enum ArrayFlags { noslices // when <<, `.noslices` will free the old data block immediately (you have to be sure, that there are *no slices* to that specific array). TODO: integrate with reference counting/compiler support for the static cases. - noshrink // when `.noslices` and `.noshrink` are *both set*, .delete(x) will NOT allocate new memory and free the old. It will just move the elements in place, and adjust .len. - nogrow // the array will never be allowed to grow past `.cap`. set `.nogrow` and `.noshrink` for a truly fixed heap array + noshrink // kept for compatibility; shrinking array operations preserve capacity by default + nogrow // the array will never be allowed to grow past `.cap` nofree // `.data` will never be freed managed // `.data` uses the builtin managed array allocation with a metadata header noscan_data // `.data` was allocated in a no-scan heap block and can stay atomic when cloned or resized @@ -560,12 +560,11 @@ fn (mut a array) prepend_many(val voidptr, size int) { unsafe { a.insert_many(0, val, size) } } -// delete deletes array element at index `i`. -// Deleting the last element uses the same in-place fast path as `.delete_last()`. -// NOTE: When deleting the last element, this operates in-place. -// Other positions create a copy of the array, skipping over the -// element at `i`, and then point the original variable to the new -// memory location. +// delete deletes array element at index `i`, preserving element order. +// For unique arrays, it shifts elements left in-place, preserves spare +// capacity, and clears the obsolete tail slot. +// If the backing buffer is shared with slices, delete creates a detached +// array, so existing slices keep their view of the old contents. // // Example: // ```v @@ -578,16 +577,19 @@ pub fn (mut a array) delete(i int) { } if i == a.len - 1 && !a.needs_unique_shrink() { a.len-- + unsafe { + vmemset(&u8(a.data) + u64(a.len) * u64(a.element_size), 0, u64(a.element_size)) + } return } a.delete_many(i, 1) } // delete_many deletes `size` elements beginning with index `i` -// NOTE: This function does NOT operate in-place. Internally, it -// creates a copy of the array, skipping over `size` elements -// starting at `i`, and then points the original variable -// to the new memory location. +// For unique arrays, it shifts elements left in-place, preserves spare +// capacity, and clears the obsolete tail range. +// If the backing buffer is shared with slices, delete_many creates a +// detached array, so existing slices keep their view of the old contents. // // Example: // ```v @@ -611,21 +613,15 @@ pub fn (mut a array) delete_many(i int, size int) { } return } - if a.flags.all(.noshrink | .noslices) && !a.needs_unique_shrink() { - unsafe { - vmemmove(&u8(a.data) + u64(i) * u64(a.element_size), &u8(a.data) + u64(i + - size) * u64(a.element_size), u64(a.len - i - size) * u64(a.element_size)) - } - a.len -= size - return - } if !a.needs_unique_shrink() { + new_len := a.len - size unsafe { vmemmove(&u8(a.data) + u64(i) * u64(a.element_size), &u8(a.data) + u64(i + size) * u64(a.element_size), u64(a.len - i - size) * u64(a.element_size)) + vmemset(&u8(a.data) + u64(new_len) * u64(a.element_size), 0, + u64(size) * u64(a.element_size)) } - a.len -= size - a.cap = a.len + a.len = new_len return } // Note: if a is [12,34], a.len = 2, a.delete(0) @@ -913,6 +909,7 @@ pub fn (mut a array) pop() voidptr { // delete_last efficiently deletes the last element of the array. // It does it simply by reducing the length of the array by 1. // If the array is empty, this will panic. +// For unique arrays, it also clears the obsolete tail slot. // See also: [trim](#array.trim) pub fn (mut a array) delete_last() { if a.len == 0 { @@ -923,6 +920,9 @@ pub fn (mut a array) delete_last() { return } a.len-- + unsafe { + vmemset(&u8(a.data) + u64(a.len) * u64(a.element_size), 0, u64(a.element_size)) + } } // slice returns an array using the same buffer as original array diff --git a/vlib/builtin/array_flags_test.v b/vlib/builtin/array_flags_test.v index c31f7c8f9..e50138f30 100644 --- a/vlib/builtin/array_flags_test.v +++ b/vlib/builtin/array_flags_test.v @@ -21,33 +21,33 @@ fn trace_delete_elements(name string, mut a []int) int { return res } -fn test_array_cap_shrinkage_after_deletion() { +fn test_array_cap_after_deletion() { mut a := [0] mut middle_cap := 0 a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] middle_cap = trace_delete_elements('normal', mut a) - assert middle_cap == 14 + assert middle_cap == 10 assert a.len == 12 - assert a.cap == 14 + assert a.cap == 20 a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] unsafe { a.flags.set(.noslices) } middle_cap = dump(trace_delete_elements('noslices', mut a)) - assert middle_cap == 14 + assert middle_cap == 10 assert a.len == 12 - assert a.cap == 14 + assert a.cap == 20 a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] unsafe { a.flags.set(.noshrink) } middle_cap = dump(trace_delete_elements('noshrink', mut a)) - assert middle_cap == 14 + assert middle_cap == 10 assert a.len == 12 - assert a.cap == 14 + assert a.cap == 20 - // Note: when *both* flags are set, the memory block for the array - // should NOT shrink on deleting array elements, thus << after the - // deletion, will still have space (till .cap is reached). + // Note: deleting array elements should not shrink the backing + // memory block, thus << after the deletion will still have space + // till .cap is reached. a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] unsafe { a.flags.set(.noslices | .noshrink) } middle_cap = dump(trace_delete_elements('both', mut a)) diff --git a/vlib/builtin/array_test.v b/vlib/builtin/array_test.v index 486c9c3c8..5196416dd 100644 --- a/vlib/builtin/array_test.v +++ b/vlib/builtin/array_test.v @@ -84,6 +84,10 @@ fn test_delete_last_uses_in_place_fast_path_for_unique_arrays() { a.delete(a.len - 1) assert a == [1, 2, 3] assert a.data == old_data + assert a.cap == 4 + unsafe { + assert (&int(a.data))[a.len] == 0 + } } fn test_delete_last_detaches_when_a_slice_exists() { @@ -96,6 +100,18 @@ fn test_delete_last_detaches_when_a_slice_exists() { assert a.data != old_data } +fn test_delete_last_clears_removed_slot_for_unique_arrays() { + mut a := [1, 2, 3, 4] + old_data := a.data + a.delete_last() + assert a == [1, 2, 3] + assert a.data == old_data + assert a.cap == 4 + unsafe { + assert (&int(a.data))[a.len] == 0 + } +} + fn test_delete_many() { mut a := [1, 2, 3, 4, 5, 6, 7, 8, 9] b := unsafe { a[2..6] } @@ -125,7 +141,11 @@ fn test_delete_many_unique_arrays_use_in_place_fast_path() { a.delete_many(1, 2) assert a == [1, 4, 5] assert a.data == old_data - assert a.cap == 3 + assert a.cap == 8 + unsafe { + assert (&int(a.data))[a.len] == 0 + assert (&int(a.data))[a.len + 1] == 0 + } } fn test_insert_detaches_parent_with_existing_slice() { -- 2.39.5