Skip to content

Optimize memory representation and operations #707

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

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft

Conversation

elopez
Copy link
Collaborator

@elopez elopez commented Apr 14, 2025

Description

An Echidna user reported continuously increasing memory while running mstore/mcopy operations. This PR attempts to resolve that by

  • avoiding freezing the full memory on mcopy
  • avoiding multiple, repeated small memory expansions, as each expansion has to copy the whole memory contents
    • this is disabled by default, to maintain the concrete memory small (as it might be symbolized at some later point), but a config value on the VM is available by anyone doing pure concrete execution (e.g. Echidna). Limited empirical testing showed 64k is a good value to use.

Checklist

  • tested locally
  • added automated tests
  • updated the docs
  • updated the changelog

buf <- freezeMemory mem
copyBytesToMemory buf sz srcOff dstOff
SymbolicMemory mem -> do
assign (#state % #memory) (SymbolicMemory $ copySlice srcOff dstOff sz mem mem)
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 this previous code results in a bit simpler representation for the symbolic case, so we might want to keep it and only modify the concrete case

@msooseth
Copy link
Collaborator

Thanks! I am looking at it. Only changing the concrete one is definitely OK. I'm trying to see what's the deal with the Symbolic. Feels like it'd be nice to unify the two, like you did.

@msooseth
Copy link
Collaborator

OK, I had a look. Either way

assign (#state % #memory) (SymbolicMemory $ copySlice srcOff dstOff sz mem mem)

Will get executed. But with your system, we first execute buf <- readMemory srcOff sz, which means:

copySlice offset' (Lit 0) size' mem mempty

which is basically another copyslice. So I'd rather keep the concrete and the symbolic separate.

Otherwise, it looks cool!

This might be desirable to keep Concrete turned Symbolic memories small
@elopez elopez changed the title mcopy: simplify operation, avoid freezing whole memory Optimize memory representation and operations Apr 15, 2025
@elopez elopez requested a review from msooseth April 16, 2025 07:25
-- We want to always grow at least a chunk, to avoid the performance impact
-- that would happen with repeated small expansion operations, as grow does
-- a larger *copy* of the vector on a new place
memory' <- VUnboxed.Mutable.grow memory $ max toAlloc vm.config.minMemoryChunk
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

maybe this should be a growth factor rather than a fixed min amount

Copy link
Collaborator

Choose a reason for hiding this comment

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

Nice find! So I had lots of issues in C++ where I was growing by a fixed chunk... (side-note: never use resize, use insert if it will grow later) not sure how relevant that is here, but it was a painful experience :D My suggestion would be to grow some factor, usually 2x is a good rule of thumb, but 1.5x can also work. But perhaps also make sure we grow at least some value (so we don't allocate by small chunks at the beginning). I'd rather avoid repeated allocations. What do you think?

@@ -44,6 +44,7 @@ vmForEthrunCreation creationCode =
, allowFFI = False
, freshAddresses = 0
, beaconRoot = 0
, minMemoryChunk = 1
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we need this controllable? We can just set it to a sane default instead? I'd rather not have config options for stuff that a good default works for. It's useful to have this during figuring out what a good default is, but later, it may become a distraction perhaps? 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.

My concern that led me to make this a tunable was symbolic execution. I'm not particularly familiar with how it is represented though so take this with a grain of salt, but what would happen if you end up having a large concrete memory due to the "optimistic growth", which then turns symbolic due to some other reason? How would that get encoded into Z3 et al? Would it hurt performance?

If my concern is unfounded I'm all for picking a good default and not having to have this option 😄

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.

2 participants