Skip to content

Commit

Permalink
[move-stdlib] Add vector::move_range native function (#14863)
Browse files Browse the repository at this point in the history
## Description

Memcopy (i.e. `ptr::copy_nonoverlapping` inside of `Vec`) is extremely efficient, and using Vec operations that use it directly is significantly faster (orders of magnitude on bigger vectors) than issuing operations in move.

Operations on `vector` that can be speed-up: `insert`, `remove`, `append`, `split_off`.

To keep amount of native functions short, instead of having native for each of those, providing one more general native function: `vector::move_range`, which is enough to support all 4 of the above, in addition to other uses. 

Internally, we shortcircuit a few special cases, for faster speed.

## How Has This Been Tested?
Full performance evaluation is in the follow-up PR: #14862

## Type of Change
- [x] Performance improvement

## Which Components or Systems Does This Change Impact?
- [x] Move/Aptos Virtual Machine
  • Loading branch information
igor-aptos authored Nov 13, 2024
1 parent 6667ac6 commit 61e6563
Show file tree
Hide file tree
Showing 13 changed files with 289 additions and 5 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ use crate::{
gas_feature_versions::{RELEASE_V1_18, RELEASE_V1_24},
gas_schedule::NativeGasParameters,
};
use aptos_gas_algebra::{InternalGas, InternalGasPerAbstractValueUnit, InternalGasPerByte};
use aptos_gas_algebra::{
InternalGas, InternalGasPerAbstractValueUnit, InternalGasPerArg, InternalGasPerByte,
};

crate::gas_schedule::macros::define_gas_parameters!(
MoveStdlibGasParameters,
Expand Down Expand Up @@ -42,5 +44,8 @@ crate::gas_schedule::macros::define_gas_parameters!(

[cmp_compare_base: InternalGas, { RELEASE_V1_24.. => "cmp.compare.base" }, 367],
[cmp_compare_per_abs_val_unit: InternalGasPerAbstractValueUnit, { RELEASE_V1_24.. => "cmp.compare.per_abs_val_unit"}, 14],

[vector_move_range_base: InternalGas, { RELEASE_V1_24.. => "vector.move_range.base" }, 4000],
[vector_move_range_per_index_moved: InternalGasPerArg, { RELEASE_V1_24.. => "vector.move_range.per_index_moved" }, 10],
]
);
2 changes: 1 addition & 1 deletion aptos-move/aptos-gas-schedule/src/ver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
/// global operations.
/// - V1
/// - TBA
pub const LATEST_GAS_FEATURE_VERSION: u64 = gas_feature_versions::RELEASE_V1_23;
pub const LATEST_GAS_FEATURE_VERSION: u64 = gas_feature_versions::RELEASE_V1_24;

pub mod gas_feature_versions {
pub const RELEASE_V1_8: u64 = 11;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ pub enum FeatureFlag {
FederatedKeyless,
TransactionSimulationEnhancement,
CollectionOwner,
NativeMemoryOperations,
EnableLoaderV2,
}

Expand Down Expand Up @@ -348,6 +349,7 @@ impl From<FeatureFlag> for AptosFeatureFlag {
AptosFeatureFlag::TRANSACTION_SIMULATION_ENHANCEMENT
},
FeatureFlag::CollectionOwner => AptosFeatureFlag::COLLECTION_OWNER,
FeatureFlag::NativeMemoryOperations => AptosFeatureFlag::NATIVE_MEMORY_OPERATIONS,
FeatureFlag::EnableLoaderV2 => AptosFeatureFlag::ENABLE_LOADER_V2,
}
}
Expand Down Expand Up @@ -492,6 +494,7 @@ impl From<AptosFeatureFlag> for FeatureFlag {
FeatureFlag::TransactionSimulationEnhancement
},
AptosFeatureFlag::COLLECTION_OWNER => FeatureFlag::CollectionOwner,
AptosFeatureFlag::NATIVE_MEMORY_OPERATIONS => FeatureFlag::NativeMemoryOperations,
AptosFeatureFlag::ENABLE_LOADER_V2 => FeatureFlag::EnableLoaderV2,
}
}
Expand Down
21 changes: 20 additions & 1 deletion aptos-move/aptos-vm-environment/src/natives.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,35 @@
use aptos_native_interface::SafeNativeBuilder;
use move_core_types::language_storage::CORE_CODE_ADDRESS;
use move_vm_runtime::native_functions::NativeFunctionTable;
use std::collections::HashSet;

/// Builds and returns all Aptos native functions.
pub fn aptos_natives_with_builder(
builder: &mut SafeNativeBuilder,
inject_create_signer_for_gov_sim: bool,
) -> NativeFunctionTable {
let vector_bytecode_instruction_methods = HashSet::from([
"empty",
"length",
"borrow",
"borrow_mut",
"push_back",
"pop_back",
"destroy_empty",
"swap",
]);

#[allow(unreachable_code)]
aptos_move_stdlib::natives::all_natives(CORE_CODE_ADDRESS, builder)
.into_iter()
.filter(|(_, name, _, _)| name.as_str() != "vector")
.filter(|(_, name, func_name, _)|
if name.as_str() == "vector" && vector_bytecode_instruction_methods.contains(func_name.as_str()) {
println!("ERROR: Tried to register as native a vector bytecode_instruction method {}, skipping.", func_name.as_str());
false
} else {
true
}
)
.chain(aptos_framework::natives::all_natives(
CORE_CODE_ADDRESS,
builder,
Expand Down
1 change: 1 addition & 0 deletions aptos-move/framework/move-stdlib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ publish = false
[dependencies]
aptos-gas-schedule = { workspace = true }
aptos-native-interface = { workspace = true }
aptos-types = { workspace = true }
move-core-types = { path = "../../../third_party/move/move-core/types" }
move-vm-runtime = { path = "../../../third_party/move/move-vm/runtime" }
move-vm-types = { path = "../../../third_party/move/move-vm/types" }
Expand Down
30 changes: 30 additions & 0 deletions aptos-move/framework/move-stdlib/doc/vector.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ the return on investment didn't seem worth it for these simple functions.
- [Function `pop_back`](#0x1_vector_pop_back)
- [Function `destroy_empty`](#0x1_vector_destroy_empty)
- [Function `swap`](#0x1_vector_swap)
- [Function `move_range`](#0x1_vector_move_range)
- [Function `singleton`](#0x1_vector_singleton)
- [Function `reverse`](#0x1_vector_reverse)
- [Function `reverse_slice`](#0x1_vector_reverse_slice)
Expand Down Expand Up @@ -340,6 +341,35 @@ Aborts if <code>i</code> or <code>j</code> is out of bounds.



</details>

<a id="0x1_vector_move_range"></a>

## Function `move_range`

Moves range of elements <code>[removal_position, removal_position + length)</code> from vector <code>from</code>,
to vector <code><b>to</b></code>, inserting them starting at the <code>insert_position</code>.
In the <code>from</code> vector, elements after the selected range are moved left to fill the hole
(i.e. range is removed, while the order of the rest of the elements is kept)
In the <code><b>to</b></code> vector, elements after the <code>insert_position</code> are moved to the right to make space for new elements
(i.e. range is inserted, while the order of the rest of the elements is kept).
Move prevents from having two mutable references to the same value, so <code>from</code> and <code><b>to</b></code> vectors are always distinct.


<pre><code><b>public</b>(<b>friend</b>) <b>fun</b> <a href="vector.md#0x1_vector_move_range">move_range</a>&lt;T&gt;(from: &<b>mut</b> <a href="vector.md#0x1_vector">vector</a>&lt;T&gt;, removal_position: u64, length: u64, <b>to</b>: &<b>mut</b> <a href="vector.md#0x1_vector">vector</a>&lt;T&gt;, insert_position: u64)
</code></pre>



<details>
<summary>Implementation</summary>


<pre><code><b>native</b> <b>public</b>(<b>friend</b>) <b>fun</b> <a href="vector.md#0x1_vector_move_range">move_range</a>&lt;T&gt;(from: &<b>mut</b> <a href="vector.md#0x1_vector">vector</a>&lt;T&gt;, removal_position: u64, length: u64, <b>to</b>: &<b>mut</b> <a href="vector.md#0x1_vector">vector</a>&lt;T&gt;, insert_position: u64);
</code></pre>



</details>

<a id="0x1_vector_singleton"></a>
Expand Down
14 changes: 14 additions & 0 deletions aptos-move/framework/move-stdlib/sources/vector.move
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,20 @@ module std::vector {
/// Aborts if `i` or `j` is out of bounds.
native public fun swap<Element>(self: &mut vector<Element>, i: u64, j: u64);

// TODO - functions here are `public(friend)` here for one release,
// and to be changed to `public` one release later.
#[test_only]
friend std::vector_tests;

/// Moves range of elements `[removal_position, removal_position + length)` from vector `from`,
/// to vector `to`, inserting them starting at the `insert_position`.
/// In the `from` vector, elements after the selected range are moved left to fill the hole
/// (i.e. range is removed, while the order of the rest of the elements is kept)
/// In the `to` vector, elements after the `insert_position` are moved to the right to make space for new elements
/// (i.e. range is inserted, while the order of the rest of the elements is kept).
/// Move prevents from having two mutable references to the same value, so `from` and `to` vectors are always distinct.
native public(friend) fun move_range<T>(from: &mut vector<T>, removal_position: u64, length: u64, to: &mut vector<T>, insert_position: u64);

/// Return an vector of size one containing element `e`.
public fun singleton<Element>(e: Element): vector<Element> {
let v = empty();
Expand Down
2 changes: 2 additions & 0 deletions aptos-move/framework/move-stdlib/src/natives/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub mod signer;
pub mod string;
#[cfg(feature = "testing")]
pub mod unit_test;
pub mod vector;

use aptos_native_interface::SafeNativeBuilder;
use move_core_types::account_address::AccountAddress;
Expand All @@ -37,6 +38,7 @@ pub fn all_natives(
add_natives!("hash", hash::make_all(builder));
add_natives!("signer", signer::make_all(builder));
add_natives!("string", string::make_all(builder));
add_natives!("vector", vector::make_all(builder));
#[cfg(feature = "testing")]
{
add_natives!("unit_test", unit_test::make_all(builder));
Expand Down
116 changes: 116 additions & 0 deletions aptos-move/framework/move-stdlib/src/natives/vector.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Copyright © Aptos Foundation
// SPDX-License-Identifier: Apache-2.0

// Copyright (c) The Diem Core Contributors
// Copyright (c) The Move Contributors
// SPDX-License-Identifier: Apache-2.0

//! Implementation of native functions (non-bytecode instructions) for vector.
use aptos_gas_schedule::gas_params::natives::move_stdlib::{
VECTOR_MOVE_RANGE_BASE, VECTOR_MOVE_RANGE_PER_INDEX_MOVED,
};
use aptos_native_interface::{
safely_pop_arg, RawSafeNative, SafeNativeBuilder, SafeNativeContext, SafeNativeError,
SafeNativeResult,
};
use aptos_types::error;
use move_core_types::gas_algebra::NumArgs;
use move_vm_runtime::native_functions::NativeFunction;
use move_vm_types::{
loaded_data::runtime_types::Type,
values::{Value, VectorRef},
};
use smallvec::{smallvec, SmallVec};
use std::collections::VecDeque;

/// Given input positions/lengths are outside of vector boundaries.
pub const EINDEX_OUT_OF_BOUNDS: u64 = 1;

/// The feature is not enabled.
pub const EFEATURE_NOT_ENABLED: u64 = 2;

/***************************************************************************************************
* native fun move_range<T>(from: &mut vector<T>, removal_position: u64, length: u64, to: &mut vector<T>, insert_position: u64)
*
* gas cost: VECTOR_MOVE_RANGE_BASE + VECTOR_MOVE_RANGE_PER_INDEX_MOVED * num_elements_to_move
*
**************************************************************************************************/
fn native_move_range(
context: &mut SafeNativeContext,
ty_args: Vec<Type>,
mut args: VecDeque<Value>,
) -> SafeNativeResult<SmallVec<[Value; 1]>> {
if !context
.get_feature_flags()
.is_native_memory_operations_enabled()
{
return Err(SafeNativeError::Abort {
abort_code: error::unavailable(EFEATURE_NOT_ENABLED),
});
}

context.charge(VECTOR_MOVE_RANGE_BASE)?;

let map_err = |_| SafeNativeError::Abort {
abort_code: error::invalid_argument(EINDEX_OUT_OF_BOUNDS),
};
let insert_position = usize::try_from(safely_pop_arg!(args, u64)).map_err(map_err)?;
let to = safely_pop_arg!(args, VectorRef);
let length = usize::try_from(safely_pop_arg!(args, u64)).map_err(map_err)?;
let removal_position = usize::try_from(safely_pop_arg!(args, u64)).map_err(map_err)?;
let from = safely_pop_arg!(args, VectorRef);

// We need to charge before executing, so fetching and checking sizes here.
// We repeat fetching and checking of the sizes inside VectorRef::move_range call as well.
// Not sure if possible to combine (as we are never doing charging there).
let to_len = to.length_as_usize(&ty_args[0])?;
let from_len = from.length_as_usize(&ty_args[0])?;

if removal_position
.checked_add(length)
.map_or(true, |end| end > from_len)
|| insert_position > to_len
{
return Err(SafeNativeError::Abort {
abort_code: EINDEX_OUT_OF_BOUNDS,
});
}

// We are moving all elements in the range, all elements after range, and all elements after insertion point.
// We are counting "length" of moving block twice, as it both gets moved out and moved in.
// From calibration testing, this seems to be a reasonable approximation of the cost of the operation.
context.charge(
VECTOR_MOVE_RANGE_PER_INDEX_MOVED
* NumArgs::new(
(from_len - removal_position)
.checked_add(to_len - insert_position)
.and_then(|v| v.checked_add(length))
.ok_or_else(|| SafeNativeError::Abort {
abort_code: EINDEX_OUT_OF_BOUNDS,
})? as u64,
),
)?;

VectorRef::move_range(
&from,
removal_position,
length,
&to,
insert_position,
&ty_args[0],
)?;

Ok(smallvec![])
}

/***************************************************************************************************
* module
**************************************************************************************************/
pub fn make_all(
builder: &SafeNativeBuilder,
) -> impl Iterator<Item = (String, NativeFunction)> + '_ {
let natives = [("move_range", native_move_range as RawSafeNative)];

builder.make_named_natives(natives)
}
10 changes: 10 additions & 0 deletions aptos-move/framework/move-stdlib/tests/vector_tests.move
Original file line number Diff line number Diff line change
Expand Up @@ -954,4 +954,14 @@ module std::vector_tests {
let v = vector[MoveOnly {}];
vector::destroy(v, |m| { let MoveOnly {} = m; })
}

#[test]
fun test_move_range_ints() {
let v = vector[3, 4, 5, 6];
let w = vector[1, 2];

V::move_range(&mut v, 1, 2, &mut w, 1);
assert!(&v == &vector[3, 6], 0);
assert!(&w == &vector[1, 4, 5, 2], 0);
}
}
Loading

0 comments on commit 61e6563

Please sign in to comment.