Skip to content

Vec::extend/from_iter: accumulate in bulk instead of per-element push_back#1893

Draft
leighmcculloch wants to merge 1 commit into
mainfrom
vec-from-iter-bulk
Draft

Vec::extend/from_iter: accumulate in bulk instead of per-element push_back#1893
leighmcculloch wants to merge 1 commit into
mainfrom
vec-from-iter-bulk

Conversation

@leighmcculloch

@leighmcculloch leighmcculloch commented Jun 4, 2026

Copy link
Copy Markdown
Member

What

The Extend impl for Vec (used by Vec::from_iter and .extend(..))
appended items by calling vec_push_back once per element. Each call is a host
call that also allocates a new intermediate vec object on the host. This change
accumulates items into a fixed-size stack buffer and flushes each full buffer
with a single bulk vec_new_from_slice + append.

Why

Same class of inefficiency that was fixed for BytesN::from in #1888
(per-element host calls where a bulk host op exists).

Net cost (baseline subtracted), measured on the guest budget with a WASM
compute-budget bench built the same way as the #1888 bench (baseline_* twin
subtracted; from_array shown as the already-bulk reference):

Bench Before CPU After CPU Δ% Before Mem After Mem Δ%
vec_from_iter_192 466,056 230,932 −50% 171,368 8,456 −95%

Bench contract and harness

Contract (one test_* cdylib crate):

#[no_std]
use soroban_sdk::{contract, contractimpl, Env, Vec};

#[contract]
pub struct Contract;

fn fp(v: &Vec) -> u32 {
    let l = v.len();
    if l == 0 { 0 } else { v.get_unchecked(0).wrapping_add(v.get_unchecked(l - 1)) }
}

#[contractimpl]
impl Contract {
    pub fn baseline_192(_e: Env) -> u32 { let d = [7u32; 192]; d[0].wrapping_add(d[191]) }
    pub fn vec_from_iter_192(e: Env) -> u32 { fp(&Vec::from_iter(&e, 0..192u32)) }
    pub fn vec_from_array_192(e: Env) -> u32 { fp(&Vec::from_array(&e, [7u32; 192])) }
}

Harness (in soroban-sdk/src/tests, #[ignore]d): register the wasm,
reset_unlimited(), call client.(), then print
cpu_instruction_cost() / memory_bytes_cost().

Run with make build-test-wasms then
cargo test --release -p soroban-sdk --lib --features testutils -- --ignored --nocapture.

Notes

The chunked approach works for any iterator and uses a fixed-size stack buffer
of Vals, so it works in no_std without alloc (the common contract case).
The from_iter doc note that claimed per-element host calls is updated. A unit
test (test_vec_from_iter_chunking) covers empty, sub-chunk,
exact-chunk-multiple and cross-chunk lengths.

Following the precedent of #1888, only the fix + correctness unit test are
committed here; the bench lives in this description rather than in the tree.

One of three related findings from sweeping the SDK for per-element host-call
patterns (see also: Vec::from_slice #1891, Vec::extend_from_slice #1892).

…_back

The `Extend` impl for `Vec` (used by `Vec::from_iter` and `.extend(..)`)
appended items by calling `vec_push_back` once per element, which is one
host call per item and allocates a new intermediate vec object on the host
every time.

Accumulate items into a fixed-size stack buffer and flush each full buffer
with a single bulk `vec_new_from_slice` + append instead. This mirrors the
zero-copy improvement made to `BytesN::from` in #1888.

Also updates the `from_iter` doc note that claimed per-element host calls,
and adds a unit test covering empty, sub-chunk, exact-chunk-multiple and
cross-chunk lengths.

https://claude.ai/code/session_0168wkXopix1LPcuzi6AS4WF
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.

1 participant