Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Update contract leaf size #450

Merged
merged 27 commits into from
May 24, 2023

Conversation

bvrooman
Copy link
Contributor

@bvrooman bvrooman commented May 18, 2023

@bvrooman bvrooman marked this pull request as ready for review May 22, 2023 22:52
@bvrooman bvrooman requested a review from a team May 22, 2023 22:56
@bvrooman bvrooman self-assigned this May 22, 2023
@@ -193,4 +194,24 @@ mod tests {
let default_root = Contract::default_state_root();
insta::assert_debug_snapshot!(default_root);
}

Copy link
Member

@Voxelot Voxelot May 23, 2023

Choose a reason for hiding this comment

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

should we have a unit test to verify padding behavior?

e.g. compare with a manually constructed leaf vs the chunking algorithm above

@xgreenx
Copy link
Collaborator

xgreenx commented May 23, 2023

I think it is a good idea to add a new test to verify padding. The change looks good for me, could you also update the changelog please?

Copy link
Collaborator

@xgreenx xgreenx left a comment

Choose a reason for hiding this comment

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

Still waiting for updated changelog=D

fuel-tx/src/contract.rs Outdated Show resolved Hide resolved
@@ -29,25 +45,17 @@ impl Contract {
B: AsRef<[u8]>,
{
let mut tree = BinaryMerkleTree::new();
let mut bytes = bytes.as_ref().to_vec();
Copy link
Collaborator

Choose a reason for hiding this comment

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

You don't need to create a vector here. You can still use a slice. But chunk during iteration, you need(maybe) to pad the last leaf.

Copy link
Contributor Author

@bvrooman bvrooman May 24, 2023

Choose a reason for hiding this comment

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

Yep, that's a good point. I updated the algorithm to allocate and pad only if the last leaf is not already a multiple of 8.

@bvrooman
Copy link
Contributor Author

Still waiting for updated changelog=D

Will update now

CHANGELOG.md Outdated Show resolved Hide resolved
Brandon Vrooman and others added 2 commits May 24, 2023 13:06
CHANGELOG.md Outdated Show resolved Hide resolved
xgreenx
xgreenx previously approved these changes May 24, 2023
Comment on lines 57 to 59
let mut padded_leaf = vec![PADDING_BYTE; padding_size];
padded_leaf[0..len].clone_from_slice(leaf);
tree.push(padded_leaf.as_ref());
Copy link
Collaborator

Choose a reason for hiding this comment

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

You can do that without vector=) You can create array [PADDING_BYTE; MULTIPLE] and only copy into first several bytes=)

Copy link
Contributor Author

@bvrooman bvrooman May 24, 2023

Choose a reason for hiding this comment

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

I'm not sure that it's enough to create [PADDING_BYTE; MULTIPLE], since your padding size will be some factor of MULTIPLE (i.e., n * MULTIPLE) if the remainder is already greater than MULTIPLE. We don't know the size at compile time, so I use a vector to accommodate that unknown size.

Edit:

I tried modifying the algorithm to:

let mut padded_leaf = [PADDING_BYTE; MULTIPLE];
padded_leaf[0..len].clone_from_slice(leaf);
tree.push(padded_leaf.as_ref());

This results in some test failures. Maybe I don't understand the suggestion exactly?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I meant the following=) But I don't know which is faster, your variant or mine=)
image

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you have a preference on which version to use? The advantage of the vector approach is that it will have a smaller average memory allocation than the array approach (0 to LEAF_SIZE - 1 vs. constant LEAF_SIZE). The advantage of the array approach is, of course, that it allocates on the stack, rather than the heap.

Copy link
Collaborator

Choose a reason for hiding this comment

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

We need to decide it based on the benchmarks=) Could you do that, please?

Copy link
Contributor Author

@bvrooman bvrooman May 24, 2023

Choose a reason for hiding this comment

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

I did some benchmarking using Criterion. I compared the two variations of the algorithm.

The first version v1 (using vec!):

...
let padding_size = next_multiple::<MULTIPLE>(len);
let mut padded_leaf = vec![PADDING_BYTE; padding_size];
padded_leaf[0..len].clone_from_slice(leaf);
tree.push(padded_leaf.as_ref());

The second version V2 (using an array):

...
let padding_size = next_multiple::<MULTIPLE>(len);
let mut padded_leaf = [PADDING_BYTE; LEAF_SIZE];
padded_leaf[0..len].clone_from_slice(leaf);
tree.push(padded_leaf[..padding_size].as_ref());

The bench setup is as follows:

const LEAF_SIZE: usize = 1024 * 16;

fn bench(c: &mut Criterion) {
    let mut rng = StdRng::seed_from_u64(0xF00D);
    let code_len = 3 * LEAF_SIZE + 100;
    let mut code = alloc::vec![0u8; code_len];
    rng.fill_bytes(code.as_mut_slice());

    let mut group = c.benchmark_group("contract");
    group.bench_with_input("root_from_code", &code, |b, code| {
        b.iter(|| Contract::root_from_code(code))
    });
}

criterion_group!(benches, bench);
criterion_main!(benches);

First, I ran the bench using v1. The output is as follows:

Running benches/bench.rs (/Volumes/Fuel/projects/FuelLabs/fuel-vm/target/release/deps/bench-7aacf08cf5869b41)
contract/root_from_code time:   [152.83 µs 153.27 µs 153.76 µs]
Found 5 outliers among 100 measurements (5.00%)
  1 (1.00%) low mild
  4 (4.00%) high mild

Second, I ran the bench using v2. the output is as follows:

Running benches/bench.rs (/Volumes/Fuel/projects/FuelLabs/fuel-vm/target/release/deps/bench-7aacf08cf5869b41)
contract/root_from_code time:   [152.03 µs 152.39 µs 152.77 µs]
                        change: [-0.2665% +0.1694% +0.6561%] (p = 0.47 > 0.05)
                        No change in performance detected.
Found 4 outliers among 100 measurements (4.00%)
  3 (3.00%) high mild
  1 (1.00%) high severe

I went back and forth between the two implementations, and further results were generally consistent with these initial benchmarks. In (almost*) every case, Criterion reports No change in performance detected.

*There were a couple of outlier tests where the difference was +/- ~30%, but I simply reran the benchmarks and these anomalies self-corrected.

In conclusion, it seems that there is negligible difference in performance between the two implementations. Since performance is not affected by implementation, I might suggest we go with v1 since we know that it will allocate less memory on average. But v2 is also good, and we can go with that if anyone feels strongly about that option.

I can also include the benchmark code in this PR if that would be helpful. Just let me know and I can commit it here.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Could you also try let code_len = 4 * LEAF_SIZE - 1; please?

If the results are the same, then I prefer a variant with an array=)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Using let code_len = 4 * LEAF_SIZE - 1;, the results are:

v1:

Running benches/bench.rs (/Volumes/Fuel/projects/FuelLabs/fuel-vm/target/release/deps/bench-7aacf08cf5869b41)
contract/root_from_code time:   [200.90 µs 201.42 µs 201.97 µs]
Found 2 outliers among 100 measurements (2.00%)
  1 (1.00%) high mild
  1 (1.00%) high severe

v2:

Running benches/bench.rs (/Volumes/Fuel/projects/FuelLabs/fuel-vm/target/release/deps/bench-7aacf08cf5869b41)
contract/root_from_code time:   [200.95 µs 201.41 µs 201.86 µs]
                        change: [-0.1991% +0.1953% +0.6080%] (p = 0.34 > 0.05)
                        No change in performance detected.
Found 5 outliers among 100 measurements (5.00%)
  3 (3.00%) high mild
  2 (2.00%) high severe

Yes, the results appear to be the same, even when changing the partial leaf size.

In that case, I will update the code to use the array version.

Please let me know if you want me to include the bench code as well and I will clean it up and commit it for review.

Copy link
Collaborator

Choose a reason for hiding this comment

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

If there is nothing special, then maybe it is better not to keep this benchmark because the chance that we need it again is low=)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sounds good. I've updated the code now to use the array version, and it should be good to go.

@bvrooman bvrooman enabled auto-merge May 24, 2023 22:58
@bvrooman bvrooman added this pull request to the merge queue May 24, 2023
Merged via the queue into master with commit b952956 May 24, 2023
@bvrooman bvrooman deleted the bvrooman/feat/update-contract-leaf-size branch May 24, 2023 23:04
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.

Use 16kib leaf sizes when calculating code roots Large contracts take a very long time to merkleize
3 participants