Skip to content

Vec::from_slice: build in bulk instead of per-element push_back#1891

Draft
leighmcculloch wants to merge 3 commits into
mainfrom
vec-from-slice-bulk
Draft

Vec::from_slice: build in bulk instead of per-element push_back#1891
leighmcculloch wants to merge 3 commits into
mainfrom
vec-from-slice-bulk

Conversation

@leighmcculloch

@leighmcculloch leighmcculloch commented Jun 4, 2026

Copy link
Copy Markdown
Member

What

Vec::from_slice previously built the Vec 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 builds the Vec with bulk
vec_new_from_slice host calls over fixed-size chunks instead.

Why

This is the same class of inefficiency that was fixed for BytesN::from in
#1888 (per-element host calls where a bulk host op exists), found by sweeping
the SDK for element-by-element host-call patterns.

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_slice_32 76,164 28,753 −62% 8,168 376 −95%
vec_from_slice_96 225,668 47,173 −79% 48,872 2,648 −95%
vec_from_slice_192 465,284 73,087 −84% 171,368 7,976 −95%

For reference, the already-bulk vec_from_array_192 net cost is 171,517 CPU /
1,656 mem, so after this change from_slice is on par with (here, even below)
the bulk constructor.

Bench contract and harness

Contract (one test_* cdylib crate):

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

#[contract]
pub struct Contract;

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

#[contractimpl]
impl Contract {
    pub fn baseline_192(_env: Env) -> u32 { let d = [7u32; 192]; d[0].wrapping_add(d[191]) }
    pub fn vec_from_slice_192(env: Env) -> u32 { fingerprint(&Vec::from_slice(&env, &[7u32; 192])) }
    pub fn vec_from_array_192(env: Env) -> u32 { fingerprint(&Vec::from_array(&env, [7u32; 192])) }
    // ... also _32 and _96 variants
}

Harness (in soroban-sdk/src/tests, #[ignore]d):

mod bench {
    use crate as soroban_sdk;
    soroban_sdk::contractimport!(file = "../target/wasm32v1-none/release/test_bench_from_slice.wasm");
}
fn report(label: &str, env: &Env) {
    let cpu = env.cost_estimate().budget().cpu_instruction_cost();
    let mem = env.cost_estimate().budget().memory_bytes_cost();
    println!("BENCH {label} cpu={cpu} mem={mem}");
}
// register the wasm, reset_unlimited(), call client.(), then report().

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

Notes

The chunked approach uses a fixed-size stack buffer of Vals, so it works in
no_std without alloc (the common contract case). A unit test
(test_vec_from_slice_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::extend_from_slice #1892, Vec::from_iter).

`Vec::from_slice` (and `vec!`-from-slice users) built the Vec 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.

Build the Vec with bulk `vec_new_from_slice` host calls over fixed-size
chunks instead. This mirrors the zero-copy improvement made to
`BytesN::from` in #1888.

Includes a WASM compute-budget bench (`tests/bench_from_slice`) comparing
`Vec::from_slice` against `Vec::from_array` (already bulk) and a baseline,
plus a unit test covering empty, sub-chunk, exact-chunk-multiple and
cross-chunk lengths.

https://claude.ai/code/session_0168wkXopix1LPcuzi6AS4WF
Copilot AI review requested due to automatic review settings June 4, 2026 23:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR optimizes soroban_sdk::Vec::from_slice by replacing per-element vec_push_back host calls with chunked bulk vec_new_from_slice calls, reducing host-call overhead and intermediate host allocations. It also adds a small unit test to validate correctness across chunk boundaries and introduces an ignored WASM compute-budget benchmark (plus its contract) to measure the improvement.

Changes:

  • Reworked Vec::from_slice to build via fixed-size chunk buffering + vec_new_from_slice, then combine chunks with vec_append.
  • Added unit test coverage for empty/sub-chunk/exact-multiple/cross-chunk lengths.
  • Added a new tests/bench_from_slice contract crate and an ignored SDK-side benchmark harness to report CPU/memory budget costs.

Reviewed changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated no comments.

Show a summary per file
File Description
soroban-sdk/src/vec.rs Implements chunked bulk construction for Vec::from_slice and adds a correctness unit test.
soroban-sdk/src/tests/bench_from_slice_wasm.rs Adds ignored compute-budget benchmarks that call into the new bench contract WASM.
soroban-sdk/src/tests.rs Registers the new benchmark module in the SDK test suite.
tests/bench_from_slice/src/lib.rs Adds the bench contract exposing baseline/from_slice/from_array entrypoints.
tests/bench_from_slice/Cargo.toml Defines the new bench contract crate and its SDK feature configuration.
Cargo.lock Adds the new test_bench_from_slice workspace package to the lockfile.

@leighmcculloch leighmcculloch marked this pull request as draft June 4, 2026 23:45
Follow the #1888 precedent of keeping the benchmark in the PR description
rather than committing a bench crate (which would require committed
cargo-expand output and trips fmt/docs/expand-test-wasms CI). Also wrap the
long vec_new_from_slice lines to satisfy rustfmt.

https://claude.ai/code/session_0168wkXopix1LPcuzi6AS4WF
from_slice converts each element by reference into a Val (like from_array)
and no longer clones, so the T: Clone bound is removed.
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