Skip to content

Cross-call EVM memory management optimization #481

@chfast

Description

@chfast

Let's consider memory "flow" for a caller running at depth d+0 calling a callee at depth d+1.

Present

This is memory buffers workflow with the maximum number of copies. But this is actually current practice for EVMC/evmone.

  1. The caller empties return data buffer R0 (its lifetime ends here).
  2. The caller calls the callee.
  3. The callee allocates EVM memory for itself M1.
  4. The callee returns output O1 (a copy of a slice of M1).
  5. The callee deallocates M1.
  6. The caller copies O1 to R0.
  7. The caller deallocates O1.

Additionally, if CALL output arguments are used the caller must copy a part of R0 to the dedicated place in its memory. There is not much we can do about this copy.

The current EVMC may eliminate M1O1 copy or O1R0 copy.

Future

What we want is:

struct memory_buffer
{
    uint8_t* ptr;
    size_t size;
    size_t capacity;
    uint8_t* return_data;
    size_t return_data_size;

    void expand(size_t new_size);
};

It can represent EVM memory and return data at the same time. And it can work like this:

  1. The caller "empties" return data buffer R0.
  2. The callee calls the callee and passes it the R0.
  3. The callee uses R0 as its EVM memory. Expands its capacity if needed.
  4. The callee returns R0 with the reference to the return data in it.
  5. The caller uses R0 as return data buffer.

The same buffer R0 is passed to every call or create the caller makes. It can grow to the value limited by the quadratic memory cost (see below).

Additionally, we can create intrusive list of memory buffers to preserve more than single buffer for "deeper" calls.

Memory size analysis

  • With 30M gas limit you can allocate ~4M of memory at depth 0.
  • But for higher depths the 63/64 rules applies so the memory size limit will be smaller.
  • Someone should model this, but keeping the chain of all buffers seems risky.

Notes

  1. The output from successful CREATE does not go to the return data buffer. The caller must remember to "empty" the buffer got from the callee in this case.
  2. The callee cannot use caller's memory spare capacity (shared buffer). The proof is trivial therefore left for the reader.
  3. This should play nicely with precompiled contracts too. E.g. identity is optional expand and single copy.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions