Skip to content

Conversation

@MariusVanDerWijden
Copy link
Member

@MariusVanDerWijden MariusVanDerWijden commented Oct 30, 2025

Looks like (in some very EVM specific tests) we spent a lot of time resizing memory.
If the underlying array if big enough, we can speed it up a bit though:

goos: linux
goarch: amd64
pkg: github.com/ethereum/go-ethereum/core/vm
cpu: Intel(R) Core(TM) Ultra 7 155U
           │ /tmp/old.txt │              /tmp/new.txt              │
           │    sec/op    │    sec/op     vs base                  │
Resize-14     6.145n ± 9%   1.854n ± 14%  -69.83% (p=0.000 n=10)

           │ /tmp/old.txt │            /tmp/new.txt             │
           │     B/op     │    B/op     vs base                 │
Resize-14      5.000 ± 0%   5.000 ± 0%       ~ (p=1.000 n=10)

           │ /tmp/old.txt │             /tmp/new.txt              │
           │  allocs/op   │ allocs/op   vs base                   │
Resize-14    0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹

From the blocktest benchmark:

     620ms     10.93s (flat, cum)  9.92% of Total
         .          .     80:func (m *Memory) Resize(size uint64) {
      30ms       60ms     81:	if uint64(m.Len()) < size {
     590ms     10.87s     82:		m.store = append(m.store, make([]byte, size-uint64(m.Len()))...)
         .          .     83:	}
         .          .     84:}


// Resize resizes the memory to size
func (m *Memory) Resize(size uint64) {
if uint64(m.Len()) < size {
Copy link
Member

Choose a reason for hiding this comment

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

As we always reuse the Memory instance without clearing.

// Free returns the memory to the pool.
func (m *Memory) Free() {
	// To reduce peak allocation, return only smaller memory instances to the pool.
	const maxBufferSize = 16 << 10
	if cap(m.store) <= maxBufferSize {
		m.store = m.store[:0]
		m.lastGasCost = 0
		memoryPool.Put(m)
	}
}

After the resize, the junks will also be visible, for instance:

  • T0; memory is empty (the underlying slice is 0x1,0x2)
  • T1; memory is resized to 2, the memory becomes [0x1, 0x2]
  • T2; the EVM only use the first byte somehow (not sure if it's possible), then the memory becomes [correct-data, junk]

Copy link
Member

Choose a reason for hiding this comment

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

diff --git a/core/vm/memory.go b/core/vm/memory.go
index 5e11e83748..2e71c16c1a 100644
--- a/core/vm/memory.go
+++ b/core/vm/memory.go
@@ -44,6 +44,8 @@ func (m *Memory) Free() {
 	// To reduce peak allocation, return only smaller memory instances to the pool.
 	const maxBufferSize = 16 << 10
 	if cap(m.store) <= maxBufferSize {
+		clear(m.store) // zero out the bytes before truncating the length
+
 		m.store = m.store[:0]
 		m.lastGasCost = 0
 		memoryPool.Put(m)
@@ -79,7 +81,11 @@ func (m *Memory) Set32(offset uint64, val *uint256.Int) {
 // Resize resizes the memory to size
 func (m *Memory) Resize(size uint64) {
 	if uint64(m.Len()) < size {
-		m.store = append(m.store, make([]byte, size-uint64(m.Len()))...)
+		if uint64(cap(m.store)) > size {
+			m.store = m.store[:size]
+		} else {
+			m.store = append(m.store, make([]byte, size-uint64(m.Len()))...)
+		}
 	}
 }
 

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants