Skip to content

execution/state: optimize transient storage zero-write fast path#20568

Open
Sahil-4555 wants to merge 2 commits intoerigontech:mainfrom
Sahil-4555:optimize/transient-storage-zero-write-fastpath
Open

execution/state: optimize transient storage zero-write fast path#20568
Sahil-4555 wants to merge 2 commits intoerigontech:mainfrom
Sahil-4555:optimize/transient-storage-zero-write-fastpath

Conversation

@Sahil-4555
Copy link
Copy Markdown
Contributor

This change optimizes the transient storage write path by avoiding per-address storage bucket creation for zero-value writes when no transient entries exist yet. Existing buckets continue to be used normally, so the change stays limited to removing unnecessary setup work from the hot path. The approach is intentionally small and local: skip map initialization when a zero-value write does not need it, while keeping the regular write path unchanged for addresses that already have transient storage state.

Result

goos: linux
goarch: amd64
pkg: github.com/erigontech/erigon/execution/state
cpu: Intel(R) Core(TM) i7-8565U CPU @ 1.80GHz
                                  │ execution/state/old_bench.txt │    execution/state/new_bench.txt     │
                                  │            sec/op             │    sec/op     vs base                │
TransientStorageSetEmpty                              24.32n ± 5%   19.21n ±  3%  -20.99% (p=0.000 n=10)
TransientStorageSetSmall                              48.63n ± 2%   31.04n ±  3%  -36.17% (p=0.000 n=10)
TransientStorageSetMedium                             48.58n ± 2%   31.09n ±  3%  -35.99% (p=0.000 n=10)
TransientStorageSetMainnetLike                        49.07n ± 2%   31.05n ±  3%  -36.72% (p=0.000 n=10)
TransientStorageSetLarge                              53.68n ± 2%   33.67n ±  2%  -37.28% (p=0.000 n=10)
TransientStorageGetMiss                               4.572n ± 3%   4.533n ±  2%        ~ (p=0.529 n=10)
TransientStorageGetHitEmpty                           11.38n ± 3%   11.76n ±  3%   +3.38% (p=0.002 n=10)
TransientStorageGetHitSmall                           21.46n ± 2%   19.15n ± 10%  -10.78% (p=0.000 n=10)
TransientStorageGetHitMedium                          21.62n ± 2%   18.96n ±  8%  -12.32% (p=0.001 n=10)
TransientStorageGetHitMainnetLike                     21.50n ± 3%   20.28n ± 14%   -5.65% (p=0.015 n=10)
TransientStorageGetHitLarge                           24.10n ± 2%   21.90n ±  5%   -9.11% (p=0.000 n=10)
TransientStorageSetDelete                             47.59n ± 2%   37.77n ±  6%  -20.63% (p=0.000 n=10)
TransientStorageMixedMainnetLike                      20.38n ± 6%   20.07n ±  7%        ~ (p=0.138 n=10)
geomean                                               25.40n        20.68n        -18.61%

                                  │ execution/state/old_bench.txt │    execution/state/new_bench.txt    │
                                  │             B/op              │    B/op     vs base                 │
TransientStorageSetEmpty                             0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
TransientStorageSetSmall                             0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
TransientStorageSetMedium                            0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
TransientStorageSetMainnetLike                       0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
TransientStorageSetLarge                             0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
TransientStorageGetMiss                              0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
TransientStorageGetHitEmpty                          0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
TransientStorageGetHitSmall                          0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
TransientStorageGetHitMedium                         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
TransientStorageGetHitMainnetLike                    0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
TransientStorageGetHitLarge                          0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
TransientStorageSetDelete                            0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
TransientStorageMixedMainnetLike                     0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
geomean                                                         ²               +0.00%                ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                                  │ execution/state/old_bench.txt │    execution/state/new_bench.txt    │
                                  │           allocs/op           │ allocs/op   vs base                 │
TransientStorageSetEmpty                             0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
TransientStorageSetSmall                             0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
TransientStorageSetMedium                            0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
TransientStorageSetMainnetLike                       0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
TransientStorageSetLarge                             0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
TransientStorageGetMiss                              0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
TransientStorageGetHitEmpty                          0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
TransientStorageGetHitSmall                          0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
TransientStorageGetHitMedium                         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
TransientStorageGetHitMainnetLike                    0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
TransientStorageGetHitLarge                          0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
TransientStorageSetDelete                            0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
TransientStorageMixedMainnetLike                     0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
geomean                                                         ²               +0.00%                ²
¹ all samples are equal
² summaries must be >0 to compute geomean

Benchmark code

// Copyright 2026 The Erigon Authors
// This file is part of Erigon.
//
// Erigon is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Erigon is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with Erigon. If not, see <http://www.gnu.org/licenses/>.

package state

import (
	"testing"

	"github.com/holiman/uint256"

	"github.com/erigontech/erigon/common"
	"github.com/erigontech/erigon/common/crypto"
	"github.com/erigontech/erigon/execution/types/accounts"
)

// mainnetLikeFixture builds deterministic addr/key/value triples that resemble
// multi-contract, multi-slot transient state usage.
func mainnetLikeFixture(n int) (addrs []accounts.Address, keys []accounts.StorageKey, values []uint256.Int) {
	addrs = make([]accounts.Address, n)
	keys = make([]accounts.StorageKey, n)
	values = make([]uint256.Int, n)
	for i := 0; i < n; i++ {
		h := crypto.Keccak256Hash([]byte("transient_addr"), []byte{byte(i), byte(i >> 8)})
		addrs[i] = accounts.InternAddress(common.BytesToAddress(h[12:]))
		keys[i] = accounts.InternKey(crypto.Keccak256Hash([]byte("transient_slot"), []byte{byte(i), byte(i >> 8)}))
		values[i].SetBytes(crypto.Keccak256Hash([]byte("transient_val"), []byte{byte(i), byte(i >> 8)}).Bytes())
	}
	return addrs, keys, values
}

func fillTransientStorage(t transientStorage, n int) {
	addrs, keys, values := mainnetLikeFixture(n)
	for i := 0; i < n; i++ {
		t.Set(addrs[i], keys[i], values[i])
	}
}

func BenchmarkTransientStorageSetEmpty(b *testing.B) {
	addrs, keys, values := mainnetLikeFixture(1)
	addr, key, value := addrs[0], keys[0], values[0]
	t := newTransientStorage()
	b.ResetTimer()
	for b.Loop() {
		t.Set(addr, key, value)
	}
}

func BenchmarkTransientStorageSetSmall(b *testing.B) {
	benchmarkTransientStorageSet(b, 8)
}

func BenchmarkTransientStorageSetMedium(b *testing.B) {
	benchmarkTransientStorageSet(b, 64)
}

func BenchmarkTransientStorageSetMainnetLike(b *testing.B) {
	benchmarkTransientStorageSet(b, 256)
}

func BenchmarkTransientStorageSetLarge(b *testing.B) {
	benchmarkTransientStorageSet(b, 1024)
}

func benchmarkTransientStorageSet(b *testing.B, size int) {
	addrs, keys, values := mainnetLikeFixture(size + 1)
	t := newTransientStorage()
	fillTransientStorage(t, size)
	addr, key, value := addrs[size], keys[size], values[size]
	b.ResetTimer()
	for b.Loop() {
		t.Set(addr, key, value)
	}
}

func BenchmarkTransientStorageGetMiss(b *testing.B) {
	t := newTransientStorage()
	addrHash := crypto.Keccak256Hash([]byte("miss_addr"))
	addr := accounts.InternAddress(common.BytesToAddress(addrHash[12:]))
	key := accounts.InternKey(crypto.Keccak256Hash([]byte("miss_key")))
	b.ResetTimer()
	for b.Loop() {
		t.Get(addr, key)
	}
}

func BenchmarkTransientStorageGetHitEmpty(b *testing.B) {
	benchmarkTransientStorageGetHit(b, 0)
}

func BenchmarkTransientStorageGetHitSmall(b *testing.B) {
	benchmarkTransientStorageGetHit(b, 8)
}

func BenchmarkTransientStorageGetHitMedium(b *testing.B) {
	benchmarkTransientStorageGetHit(b, 64)
}

func BenchmarkTransientStorageGetHitMainnetLike(b *testing.B) {
	benchmarkTransientStorageGetHit(b, 256)
}

func BenchmarkTransientStorageGetHitLarge(b *testing.B) {
	benchmarkTransientStorageGetHit(b, 1024)
}

func benchmarkTransientStorageGetHit(b *testing.B, size int) {
	addrs, keys, _ := mainnetLikeFixture(size + 1)
	t := newTransientStorage()
	fillTransientStorage(t, size)
	addr, key := addrs[size], keys[size]
	t.Set(addr, key, *uint256.NewInt(0xdeadbeef))
	b.ResetTimer()
	for b.Loop() {
		t.Get(addr, key)
	}
}

func BenchmarkTransientStorageSetDelete(b *testing.B) {
	addrs, keys, values := mainnetLikeFixture(1)
	addr, key, value := addrs[0], keys[0], values[0]
	t := newTransientStorage()
	t.Set(addr, key, value)
	b.ResetTimer()
	for b.Loop() {
		t.Set(addr, key, uint256.Int{})
		t.Set(addr, key, value)
	}
}

func BenchmarkTransientStorageMixedMainnetLike(b *testing.B) {
	const size = 128
	addrs, keys, values := mainnetLikeFixture(size)
	t := newTransientStorage()
	for i := 0; i < size; i++ {
		t.Set(addrs[i], keys[i], values[i])
	}
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		idx := i % size
		t.Get(addrs[idx], keys[idx])
	}
}

@Sahil-4555 Sahil-4555 force-pushed the optimize/transient-storage-zero-write-fastpath branch from bd19cde to c206748 Compare April 15, 2026 04:15
@AskAlexSharov AskAlexSharov requested a review from Copilot April 15, 2026 04:31
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Optimizes EIP-1153 transient storage writes by avoiding per-address Storage map creation when a zero-value write is issued for an address that has no transient entries yet.

Changes:

  • Add a fast path in transientStorage.Set to return early on zero-value writes when the address bucket does not exist.
  • Preserve existing behavior for non-zero writes and for zero writes when an address bucket already exists.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@yperbasis yperbasis added this to the 3.5.0 milestone Apr 15, 2026
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