Skip to content

Zero-copy improvement for U256 and I256#1867

Open
dkcumming wants to merge 4 commits into
stellar:mainfrom
dkcumming:dc/U256-improvement
Open

Zero-copy improvement for U256 and I256#1867
dkcumming wants to merge 4 commits into
stellar:mainfrom
dkcumming:dc/U256-improvement

Conversation

@dkcumming
Copy link
Copy Markdown

What

Functions U256::to_u128, U256::from_u128, I256::to_i128, I256::from_i128 are changed to read the {U,I}256Val directly, instead of using Bytes as an intermediary.

Why

The Bytes route invokes memory allocation host functions which are expensive. Using the {U,I}256Val directly is a zero-copy approach that avoids memory allocation and so consumes less resources. This is observed by some simple benchmarks:

Bench Before CPU After CPU Δ % Before Mem After Mem Δ %
baseline 329,285 326,974 -0.7% 1,219,606 1,150,203 -5.7%
u256_from_u128_max 340,811 330,954 -2.9% 1,220,190 1,150,363 -5.7%
u256_to_u128_fits 485,986 332,682 -31.5% 1,224,270 1,150,363 -6.0%
u256_to_u128_overflow 484,433 330,265 -31.8% 1,224,166 1,150,259 -6.0%
i256_from_i128_min 340,839 330,974 -2.9% 1,220,190 1,150,363 -5.7%
i256_to_i128_pos 486,082 332,806 -31.5% 1,224,270 1,150,363 -6.0%
i256_to_i128_neg 486,082 332,806 -31.5% 1,224,270 1,150,363 -6.0%

The harnesses that were used for benching are in the details tab that follows

Details

The contract:

#![no_std]
use soroban_sdk::{contract, contractimpl, Env, I256, U256};

#[contract]
pub struct Contract;

#[contractimpl]
impl Contract {
    pub fn baseline(_env: Env) {}

    pub fn u256_from_u128(env: Env, x: u128) -> U256 {
        U256::from_u128(&env, x)
    }

    pub fn u256_to_u128(_env: Env, v: U256) -> Option<u128> {
        v.to_u128()
    }

    pub fn i256_from_i128(env: Env, x: i128) -> I256 {
        I256::from_i128(&env, x)
    }

    pub fn i256_to_i128(_env: Env, v: I256) -> Option<i128> {
        v.to_i128()
    }
}

The benchmark harnesses:

//! WASM compute-budget benches for U256 / I256 <-> u128 / i128.
//!
//! Run with:
//! make build-test-wasms
//! cargo test --release --package soroban-sdk --lib --features testutils \
//!   -- tests::num_bench_wasm --ignored --nocapture
use crate::{Env, I256, U256};

mod num_bench {
    use crate as soroban_sdk;
    soroban_sdk::contractimport!(file = "../target/wasm32v1-none/release/test_num_bench.wasm");
}

#[test]
#[ignore]
fn bench_baseline() {
    let env = Env::default();
    let id = env.register(num_bench::WASM, ());
    let client = num_bench::Client::new(&env, &id);
    env.cost_estimate().budget().reset_unlimited();
    client.baseline();
    println!("=== baseline ===");
    env.cost_estimate().budget().print();
}

#[test]
#[ignore]
fn bench_u256_from_u128_max() {
    let env = Env::default();
    let id = env.register(num_bench::WASM, ());
    let client = num_bench::Client::new(&env, &id);
    env.cost_estimate().budget().reset_unlimited();
    let _ = client.u256_from_u128(&u128::MAX);
    println!("=== u256_from_u128_max ===");
    env.cost_estimate().budget().print();
}

#[test]
#[ignore]
fn bench_u256_to_u128_fits() {
    let env = Env::default();
    let id = env.register(num_bench::WASM, ());
    let client = num_bench::Client::new(&env, &id);
    let v = U256::from_u128(&env, u128::MAX);
    env.cost_estimate().budget().reset_unlimited();
    let _ = client.u256_to_u128(&v);
    println!("=== u256_to_u128_fits ===");
    env.cost_estimate().budget().print();
}

#[test]
#[ignore]
fn bench_u256_to_u128_overflow() {
    let env = Env::default();
    let id = env.register(num_bench::WASM, ());
    let client = num_bench::Client::new(&env, &id);
    let v = U256::from_u128(&env, u128::MAX).mul(&U256::from_u32(&env, 8));
    env.cost_estimate().budget().reset_unlimited();
    let _ = client.u256_to_u128(&v);
    println!("=== u256_to_u128_overflow ===");
    env.cost_estimate().budget().print();
}

#[test]
#[ignore]
fn bench_i256_from_i128_min() {
    let env = Env::default();
    let id = env.register(num_bench::WASM, ());
    let client = num_bench::Client::new(&env, &id);
    env.cost_estimate().budget().reset_unlimited();
    let _ = client.i256_from_i128(&i128::MIN);
    println!("=== i256_from_i128_min ===");
    env.cost_estimate().budget().print();
}

#[test]
#[ignore]
fn bench_i256_to_i128_pos() {
    let env = Env::default();
    let id = env.register(num_bench::WASM, ());
    let client = num_bench::Client::new(&env, &id);
    let v = I256::from_i128(&env, i128::MAX);
    env.cost_estimate().budget().reset_unlimited();
    let _ = client.i256_to_i128(&v);
    println!("=== i256_to_i128_pos ===");
    env.cost_estimate().budget().print();
}

#[test]
#[ignore]
fn bench_i256_to_i128_neg() {
    let env = Env::default();
    let id = env.register(num_bench::WASM, ());
    let client = num_bench::Client::new(&env, &id);
    let v = I256::from_i128(&env, i128::MIN);
    env.cost_estimate().budget().reset_unlimited();
    let _ = client.i256_to_i128(&v);
    println!("=== i256_to_i128_neg ===");
    env.cost_estimate().budget().print();
}

Known limitations

This approach does not seem to come with a tradeoff, and it still might not be completely optimised. But it offers a clear improvement. This was a targeted investigation and is not comprehensive in scope, there may be other places where similar improvements can be made.

Copilot AI review requested due to automatic review settings May 11, 2026 20:19
@dkcumming dkcumming changed the title Dc/u256 improvement Zero-copy improvement for U256 and I256 May 11, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 U256/I256u128/i128 conversions in the Soroban Rust SDK by avoiding Bytes intermediates and instead extracting/constructing values directly via {U,I}256Val / {U,I}256Object, reducing host allocations and improving compute/memory budget usage.

Changes:

  • Reworked U256::{from_u128,to_u128} to construct/deconstruct using 64-bit pieces and direct object field access.
  • Reworked I256::{from_i128,to_i128} similarly, including sign-extension handling for i128 into i256.
  • Updated internal imports to include U256Object / I256Object for direct object access.

Comment thread soroban-sdk/src/num.rs
Comment thread soroban-sdk/src/num.rs
Comment on lines +258 to +261
// If v is U256Small it can be converted directly
if let Ok(small) = U256Small::try_from(v) {
return Some(u64::from(small) as u128);
}
Comment thread soroban-sdk/src/num.rs
Comment thread soroban-sdk/src/num.rs
Comment on lines +518 to +521
// If v is I256Small it can be converted directly
if let Ok(small) = I256Small::try_from(v) {
return Some(i64::from(small) as i128);
}
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.

This seems reasonable to address if that's actually not covered

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

There's some benefit to filling these test gaps ahead of merging this PR.

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.

4 participants