Skip to content
This repository has been archived by the owner on Nov 6, 2020. It is now read-only.

Allow single opcode stepping for EVM #9051

Merged
merged 23 commits into from
Aug 13, 2018
Merged

Allow single opcode stepping for EVM #9051

merged 23 commits into from
Aug 13, 2018

Conversation

sorpaas
Copy link
Collaborator

@sorpaas sorpaas commented Jul 5, 2018

rel #6744, fixes #9035

This is a prerequisite for implementing EVM resume and callstack refactoring. In particular, a new function Interpreter::step(&mut Ext) is added. This function step one additional opcode, and can be used plainly. Other notable changes:

  • ActionParams is taken when initializing a VM, but not when calling exec. This is because we need to allocate stack, gasometer, etc before exec is called.
  • The ability to "reuse" a VM after an execution is removed. We never did that in our code. Allocation (either on stack or heap) for various VM structs need to happen anyway, so "reusing" doesn't provide performance benefits (except maybe reusing the previous VM's memory heap region).

@sorpaas sorpaas added A0-pleasereview 🤓 Pull request needs code review. F6-refactor 📚 Code needs refactoring. M4-core ⛓ Core client code / Rust. labels Jul 5, 2018
@sorpaas sorpaas added this to the 1.12 milestone Jul 5, 2018
@sorpaas
Copy link
Collaborator Author

sorpaas commented Jul 10, 2018

Here's the benchmark results:

jsontests benchmarks:

{'ackermann31-INT': 0.9897959183673469,
 'ackermann32-INT': 0.9167478091528725,
 'ackermann33-INT': 0.9596136962247586,
 'fibonacci10-INT': 1.0860832137733143,
 'fibonacci16-INT': 0.9300284017557449,
 'loop-add-10M-INT': 0.9226587206771758,
 'loop-divadd-10M-INT': 0.9229079794970425,
 'loop-divadd-unr100-10M-INT': 0.8998631955175739,
 'loop-exp-16b-100k-INT': 1.0188090390141178,
 'loop-exp-1b-1M-INT': 0.9923591588497622,
 'loop-exp-2b-100k-INT': 0.9466506030511994,
 'loop-exp-32b-100k-INT': 1.0991487959483188,
 'loop-exp-4b-100k-INT': 1.12556930650952,
 'loop-exp-8b-100k-INT': 1.1088218849334053,
 'loop-exp-nop-1M-INT': 0.8219694300785856,
 'loop-mul-INT': 0.8684106995830124,
 'loop-mulmod-2M-INT': 0.9754786210274101,
 'manyFunctions100-INT': 0.8409570724841661}

Uint benchmark (master):

Output: 0x606060405200
Gas used: 29fb170
Time: 0.407322134s
^^^^ usize
Output: 0x606060405200
Gas used: 29fb170
Time: 0.418064895s
^^^^ U256
Output: 0x606060405200
Gas used: 8865053
Time: 1.412694181s
^^^^ usize
Output: 0x606060405200
Gas used: 8865053
Time: 1.473797155s
^^^^ U256

Uint benchmark (PR):

Output: 0x606060405200
Gas used: 29fb170
Time: 0.472320852s
^^^^ usize
Output: 0x606060405200
Gas used: 29fb170
Time: 0.489357480s
^^^^ U256
Output: 0x606060405200
Gas used: 8865053
Time: 1.608301138s
^^^^ usize
Output: 0x606060405200
Gas used: 8865053
Time: 1.618444041s
^^^^ U256

This gist contains the python code used to format the result.

@5chdn 5chdn added the A1-onice 🌨 Pull request is reviewed well, but should not yet be merged. label Jul 10, 2018
@5chdn 5chdn modified the milestones: 2.0, 2.1 Jul 10, 2018
@5chdn 5chdn removed the A1-onice 🌨 Pull request is reviewed well, but should not yet be merged. label Jul 11, 2018
@5chdn 5chdn requested review from tomusdrw and debris July 13, 2018 10:26
Copy link
Collaborator

@tomusdrw tomusdrw left a comment

Choose a reason for hiding this comment

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

Looks really good, couple of minor grumbles.

/// Execute a single step on the VM.
#[inline(always)]
pub fn step(&mut self, ext: &mut vm::Ext) -> InterpreterResult {
macro_rules! try_or_done {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is it really worth to have a macro for that?
Maybe sth like this:

#[inline(always)]
fn try_or_done<F>(x: Result<F, vm::Error>) -> Result<F, InterpreterResult> {
 x.map_err(|e| InterpreterResult::Done(Err(e))
}

// and used like this:
try_or_done(self.verify_instruction(ext, instruction, info))?;

Or maybe even add From<vm::Error> for InterpreterResult and convert errors automatically via ??

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The issue is that ? only works for Result like things with Ok and Err. So we can't use any ? syntax in step right now, unless we change InterpreterResult to be a Result<A, B>. This indeed can be done via:

  • Return Ok(result), which is the same case for InterpreterResult::Done(_).
  • Return error case for InterpreterResult::Stopped and InterpreterResult::Continue.

However, I think it may be a little bit confusing as Ok/Err do not really apply here.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh, that's true. Forgot that it only works for Result on the return type as well, anyway might be worth extracting the stuff in inner to a separate function and maybe use a helper function instead of a macro. What do you think?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think I found a way: we can return Result<(), InterpreterResult> as the inner function and always return Err. The compiler would happily accept ? syntax with that. And we would just need to make sure the inner function never returns Ok(()).

This detail is wrapped in step_inner so it doesn't affect public interface.


let info = instruction.info();
self.verify_instruction(ext, instruction, info, &stack)?;
if instruction.is_none() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

let instruction = match instruction {
  Some(i) => i,
  None => return vm::Error::BadInstruction { .. }.into();
}

let instruction = Instruction::from_u8(opcode);
reader.position += 1;
let result = {
let mut inner = || {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why do we need the inner closure?

Oh, I see to be able to use return maybe it's worth to move it to a separate function?

let result = {
let mut inner = || {
// This case is needed because code length can be zero.
if self.reader.position >= self.reader.len() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Seems that we are checking that twice now, can't this be moved as some initial step? The reader.position is changed only during regular execution and jumps, right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I moved it up to check just reader.len() == 0. That check still needs to happen every time step is called, otherwise we'll need an additional "interpreter state" (which might actually cost more memory).

}

fn verify_instruction(&self, ext: &vm::Ext, instruction: Instruction, info: &InstructionInfo, stack: &Stack<U256>) -> vm::Result<()> {
fn verify_instruction(&self, ext: &vm::Ext, instruction: Instruction, info: &InstructionInfo) -> vm::Result<()> {
let schedule = ext.schedule();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Schedule could probably be cached on self level, less virtual calls.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The issue is that for some use cases of step, like pausing, we would want to take back the Ext reference, possibly do some modifications to it, before passing it back to continue the execution.

  1. If we take a reference of Schedule and put it on self level, it means that this single Ext is not usable again for the whole lifecycle of the Interpreter.
  2. Or we need to clone the Schedule, which may also be expensive.

I think virtual calls might not be that expensive. And with (1), one optimization we can do is to split Ext up to a) interpreter-specific Ext, and b) transaction-level Ext. I think that may be a good trade-off compared with additional virtual calls during each step.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Makes sense. I thought that we already clone schedule on ext.schedule() but now saw it's only borrowed.

@@ -102,127 +103,234 @@ enum InstructionResult<Gas> {
StopExecution,
}

/// ActionParams without code, so that it can be feed into CodeReader.
#[derive(Debug)]
struct InterpreterParams {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why we need this? Couldn't we just use ActionParams and add some info that the code is always None?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Actually on a second though, it's better to have it like that - prevents future errors of accessing the code.

@tomusdrw tomusdrw added A5-grumble 🔥 Pull request has minor issues that must be addressed before merging. and removed A0-pleasereview 🤓 Pull request needs code review. labels Jul 17, 2018
@5chdn 5chdn modified the milestones: 2.1, 2.2 Jul 17, 2018
@sorpaas sorpaas added A0-pleasereview 🤓 Pull request needs code review. A3-inprogress ⏳ Pull request is in progress. No review needed at this stage. and removed A5-grumble 🔥 Pull request has minor issues that must be addressed before merging. A0-pleasereview 🤓 Pull request needs code review. labels Jul 18, 2018
@sorpaas sorpaas added A0-pleasereview 🤓 Pull request needs code review. and removed A3-inprogress ⏳ Pull request is in progress. No review needed at this stage. labels Jul 24, 2018
@sorpaas
Copy link
Collaborator Author

sorpaas commented Jul 24, 2018

A new change on this PR is that I made Interpreter::new to always succeeds. We only had one possible Err case -- gasometer, if usize cost type is used, can directly return out of gas error. This is changed to delay the error return to step -- by making gasometer an option, and on step, if we found gasometer to be option, then directly return error.

The rationale for this is that I found out if Interpreter::new can return error, it would significantly complicate resumable Executive implementation.

Copy link
Contributor

@andresilva andresilva left a comment

Choose a reason for hiding this comment

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

LGTM

@sorpaas sorpaas merged commit 9c595af into master Aug 13, 2018
@sorpaas sorpaas deleted the sp-vm-step branch August 13, 2018 20:06
@5chdn 5chdn added A8-looksgood 🦄 Pull request is reviewed well. and removed A0-pleasereview 🤓 Pull request needs code review. F6-refactor 📚 Code needs refactoring. labels Aug 23, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
A8-looksgood 🦄 Pull request is reviewed well. M4-core ⛓ Core client code / Rust.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add 'Step' to VM?
4 participants