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()))...)
+		}
 	}
 }
 

Copy link
Contributor

@fjl fjl left a comment

Choose a reason for hiding this comment

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

I made two changes:

  • use slicing if cap(m.store) == size
  • use len(m.store) instead of m.Len(). since we use cap(m.store), we should access the length of the slice in the same way.

@fjl fjl added this to the 1.16.8 milestone Nov 26, 2025
@fjl fjl merged commit 3bbf5f5 into ethereum:master Nov 26, 2025
7 of 8 checks passed
fjl added a commit to lightclient/go-ethereum that referenced this pull request Nov 28, 2025
Looks like (in some very EVM specific tests) we spent a lot of time
resizing memory. If the underlying array is big enough, we can speed it 
up a bit by simply slicing the memory.

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:}

---------

Co-authored-by: Felix Lange <fjl@twurst.com>
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.

3 participants