core/types: reduce allocations for transaction comparison (#31912)
Some checks are pending
/ Linux Build (push) Waiting to run
/ Linux Build (arm) (push) Waiting to run
/ Windows Build (push) Waiting to run
/ Docker Image (push) Waiting to run

This PR should reduce overall allocations of a running node by ~10
percent. Since most allocations are coming from the re-heaping of the
transaction pool.

```
(pprof) list EffectiveGasTipCmp
Total: 38197204475
ROUTINE ======================== github.com/ethereum/go-ethereum/core/types.(*Transaction).EffectiveGasTipCmp in github.com/ethereum/go-ethereum/core/types/transaction.go
         0 3766837369 (flat, cum)  9.86% of Total
         .          .    386:func (tx *Transaction) EffectiveGasTipCmp(other *Transaction, baseFee *big.Int) int {
         .          .    387: if baseFee == nil {
         .          .    388:  return tx.GasTipCapCmp(other)
         .          .    389: }
         .          .    390: // Use more efficient internal method.
         .          .    391: txTip, otherTip := new(big.Int), new(big.Int)
         . 1796172553    392: tx.calcEffectiveGasTip(txTip, baseFee)
         . 1970664816    393: other.calcEffectiveGasTip(otherTip, baseFee)
         .          .    394: return txTip.Cmp(otherTip)
         .          .    395:}
         .          .    396:
         .          .    397:// EffectiveGasTipIntCmp compares the effective gasTipCap of a transaction to the given gasTipCap.
         .          .    398:func (tx *Transaction) EffectiveGasTipIntCmp(other *big.Int, baseFee *big.Int) int {
```

This PR reduces the allocations for comparing two transactions from 2 to
0:
```
goos: linux
goarch: amd64
pkg: github.com/ethereum/go-ethereum/core/types
cpu: Intel(R) Core(TM) Ultra 7 155U
                               │ /tmp/old.txt │            /tmp/new.txt             │
                               │    sec/op    │   sec/op     vs base                │
EffectiveGasTipCmp/Original-14    64.67n ± 2%   25.13n ± 9%  -61.13% (p=0.000 n=10)

                               │ /tmp/old.txt │            /tmp/new.txt            │
                               │     B/op     │   B/op     vs base                 │
EffectiveGasTipCmp/Original-14     16.00 ± 0%   0.00 ± 0%  -100.00% (p=0.000 n=10)

                               │ /tmp/old.txt │            /tmp/new.txt             │
                               │  allocs/op   │ allocs/op   vs base                 │
EffectiveGasTipCmp/Original-14     2.000 ± 0%   0.000 ± 0%  -100.00% (p=0.000 n=10)
```

It also speeds up the process by ~60%

There are two minor caveats with this PR:
- We change the API for `EffectiveGasTipCmp` and `EffectiveGasTipIntCmp`
(which are probably not used by much)
- We slightly change the behavior of `tx.EffectiveGasTip` when it
returns an error. It would previously return a negative number on error,
now it does not (since uint256 does not allow for negative numbers)

---------

Signed-off-by: Csaba Kiraly <csaba.kiraly@gmail.com>
Co-authored-by: Csaba Kiraly <csaba.kiraly@gmail.com>
This commit is contained in:
Marius van der Wijden 2025-08-22 10:09:25 +02:00 committed by GitHub
parent f3467d1e63
commit 10421edf3e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 95 additions and 46 deletions

View file

@ -514,26 +514,15 @@ func (pool *LegacyPool) Pending(filter txpool.PendingFilter) map[common.Address]
pool.mu.Lock() pool.mu.Lock()
defer pool.mu.Unlock() defer pool.mu.Unlock()
// Convert the new uint256.Int types to the old big.Int ones used by the legacy pool
var (
minTipBig *big.Int
baseFeeBig *big.Int
)
if filter.MinTip != nil {
minTipBig = filter.MinTip.ToBig()
}
if filter.BaseFee != nil {
baseFeeBig = filter.BaseFee.ToBig()
}
pending := make(map[common.Address][]*txpool.LazyTransaction, len(pool.pending)) pending := make(map[common.Address][]*txpool.LazyTransaction, len(pool.pending))
for addr, list := range pool.pending { for addr, list := range pool.pending {
txs := list.Flatten() txs := list.Flatten()
// If the miner requests tip enforcement, cap the lists now // If the miner requests tip enforcement, cap the lists now
if minTipBig != nil || filter.GasLimitCap != 0 { if filter.MinTip != nil || filter.GasLimitCap != 0 {
for i, tx := range txs { for i, tx := range txs {
if minTipBig != nil { if filter.MinTip != nil {
if tx.EffectiveGasTipIntCmp(minTipBig, baseFeeBig) < 0 { if tx.EffectiveGasTipIntCmp(filter.MinTip, filter.BaseFee) < 0 {
txs = txs[:i] txs = txs[:i]
break break
} }

View file

@ -475,7 +475,7 @@ func (l *list) subTotalCost(txs []*types.Transaction) {
// then the heap is sorted based on the effective tip based on the given base fee. // then the heap is sorted based on the effective tip based on the given base fee.
// If baseFee is nil then the sorting is based on gasFeeCap. // If baseFee is nil then the sorting is based on gasFeeCap.
type priceHeap struct { type priceHeap struct {
baseFee *big.Int // heap should always be re-sorted after baseFee is changed baseFee *uint256.Int // heap should always be re-sorted after baseFee is changed
list []*types.Transaction list []*types.Transaction
} }
@ -677,6 +677,10 @@ func (l *pricedList) Reheap() {
// SetBaseFee updates the base fee and triggers a re-heap. Note that Removed is not // SetBaseFee updates the base fee and triggers a re-heap. Note that Removed is not
// necessary to call right before SetBaseFee when processing a new block. // necessary to call right before SetBaseFee when processing a new block.
func (l *pricedList) SetBaseFee(baseFee *big.Int) { func (l *pricedList) SetBaseFee(baseFee *big.Int) {
l.urgent.baseFee = baseFee base := new(uint256.Int)
if baseFee != nil {
base.SetFromBig(baseFee)
}
l.urgent.baseFee = base
l.Reheap() l.Reheap()
} }

View file

@ -28,6 +28,7 @@ import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rlp"
"github.com/holiman/uint256"
) )
var ( var (
@ -36,6 +37,7 @@ var (
ErrInvalidTxType = errors.New("transaction type not valid in this context") ErrInvalidTxType = errors.New("transaction type not valid in this context")
ErrTxTypeNotSupported = errors.New("transaction type not supported") ErrTxTypeNotSupported = errors.New("transaction type not supported")
ErrGasFeeCapTooLow = errors.New("fee cap less than base fee") ErrGasFeeCapTooLow = errors.New("fee cap less than base fee")
ErrUint256Overflow = errors.New("bigint overflow, too large for uint256")
errShortTypedTx = errors.New("typed transaction too short") errShortTypedTx = errors.New("typed transaction too short")
errInvalidYParity = errors.New("'yParity' field must be 0 or 1") errInvalidYParity = errors.New("'yParity' field must be 0 or 1")
errVYParityMismatch = errors.New("'v' and 'yParity' fields do not match") errVYParityMismatch = errors.New("'v' and 'yParity' fields do not match")
@ -352,54 +354,66 @@ func (tx *Transaction) GasTipCapIntCmp(other *big.Int) int {
} }
// EffectiveGasTip returns the effective miner gasTipCap for the given base fee. // EffectiveGasTip returns the effective miner gasTipCap for the given base fee.
// Note: if the effective gasTipCap is negative, this method returns both error // Note: if the effective gasTipCap would be negative, this method
// the actual negative value, _and_ ErrGasFeeCapTooLow // returns ErrGasFeeCapTooLow, and value is undefined.
func (tx *Transaction) EffectiveGasTip(baseFee *big.Int) (*big.Int, error) { func (tx *Transaction) EffectiveGasTip(baseFee *big.Int) (*big.Int, error) {
dst := new(big.Int) dst := new(uint256.Int)
err := tx.calcEffectiveGasTip(dst, baseFee) base := new(uint256.Int)
return dst, err if baseFee != nil {
if base.SetFromBig(baseFee) {
return nil, ErrUint256Overflow
}
}
err := tx.calcEffectiveGasTip(dst, base)
return dst.ToBig(), err
} }
// calcEffectiveGasTip calculates the effective gas tip of the transaction and // calcEffectiveGasTip calculates the effective gas tip of the transaction and
// saves the result to dst. // saves the result to dst.
func (tx *Transaction) calcEffectiveGasTip(dst *big.Int, baseFee *big.Int) error { func (tx *Transaction) calcEffectiveGasTip(dst *uint256.Int, baseFee *uint256.Int) error {
if baseFee == nil { if baseFee == nil {
dst.Set(tx.inner.gasTipCap()) if dst.SetFromBig(tx.inner.gasTipCap()) {
return ErrUint256Overflow
}
return nil return nil
} }
var err error var err error
gasFeeCap := tx.inner.gasFeeCap() if dst.SetFromBig(tx.inner.gasFeeCap()) {
if gasFeeCap.Cmp(baseFee) < 0 { return ErrUint256Overflow
}
if dst.Cmp(baseFee) < 0 {
err = ErrGasFeeCapTooLow err = ErrGasFeeCapTooLow
} }
dst.Sub(gasFeeCap, baseFee) dst.Sub(dst, baseFee)
gasTipCap := tx.inner.gasTipCap() gasTipCap := new(uint256.Int)
if gasTipCap.SetFromBig(tx.inner.gasTipCap()) {
return ErrUint256Overflow
}
if gasTipCap.Cmp(dst) < 0 { if gasTipCap.Cmp(dst) < 0 {
dst.Set(gasTipCap) dst.Set(gasTipCap)
} }
return err return err
} }
// EffectiveGasTipCmp compares the effective gasTipCap of two transactions assuming the given base fee. func (tx *Transaction) EffectiveGasTipCmp(other *Transaction, baseFee *uint256.Int) int {
func (tx *Transaction) EffectiveGasTipCmp(other *Transaction, baseFee *big.Int) int {
if baseFee == nil { if baseFee == nil {
return tx.GasTipCapCmp(other) return tx.GasTipCapCmp(other)
} }
// Use more efficient internal method. // Use more efficient internal method.
txTip, otherTip := new(big.Int), new(big.Int) txTip, otherTip := new(uint256.Int), new(uint256.Int)
tx.calcEffectiveGasTip(txTip, baseFee) tx.calcEffectiveGasTip(txTip, baseFee)
other.calcEffectiveGasTip(otherTip, baseFee) other.calcEffectiveGasTip(otherTip, baseFee)
return txTip.Cmp(otherTip) return txTip.Cmp(otherTip)
} }
// EffectiveGasTipIntCmp compares the effective gasTipCap of a transaction to the given gasTipCap. // EffectiveGasTipIntCmp compares the effective gasTipCap of a transaction to the given gasTipCap.
func (tx *Transaction) EffectiveGasTipIntCmp(other *big.Int, baseFee *big.Int) int { func (tx *Transaction) EffectiveGasTipIntCmp(other *uint256.Int, baseFee *uint256.Int) int {
if baseFee == nil { if baseFee == nil {
return tx.GasTipCapIntCmp(other) return tx.GasTipCapIntCmp(other.ToBig())
} }
txTip := new(big.Int) txTip := new(uint256.Int)
tx.calcEffectiveGasTip(txTip, baseFee) tx.calcEffectiveGasTip(txTip, baseFee)
return txTip.Cmp(other) return txTip.Cmp(other)
} }

View file

@ -31,6 +31,7 @@ import (
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rlp"
"github.com/holiman/uint256"
) )
// The values in those tests are from the Transaction Tests // The values in those tests are from the Transaction Tests
@ -609,12 +610,12 @@ func BenchmarkEffectiveGasTip(b *testing.B) {
Data: nil, Data: nil,
} }
tx, _ := SignNewTx(key, signer, txdata) tx, _ := SignNewTx(key, signer, txdata)
baseFee := big.NewInt(1000000000) // 1 gwei baseFee := uint256.NewInt(1000000000) // 1 gwei
b.Run("Original", func(b *testing.B) { b.Run("Original", func(b *testing.B) {
b.ReportAllocs() b.ReportAllocs()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
_, err := tx.EffectiveGasTip(baseFee) _, err := tx.EffectiveGasTip(baseFee.ToBig())
if err != nil { if err != nil {
b.Fatal(err) b.Fatal(err)
} }
@ -623,7 +624,7 @@ func BenchmarkEffectiveGasTip(b *testing.B) {
b.Run("IntoMethod", func(b *testing.B) { b.Run("IntoMethod", func(b *testing.B) {
b.ReportAllocs() b.ReportAllocs()
dst := new(big.Int) dst := new(uint256.Int)
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
err := tx.calcEffectiveGasTip(dst, baseFee) err := tx.calcEffectiveGasTip(dst, baseFee)
if err != nil { if err != nil {
@ -634,9 +635,6 @@ func BenchmarkEffectiveGasTip(b *testing.B) {
} }
func TestEffectiveGasTipInto(t *testing.T) { func TestEffectiveGasTipInto(t *testing.T) {
signer := LatestSigner(params.TestChainConfig)
key, _ := crypto.GenerateKey()
testCases := []struct { testCases := []struct {
tipCap int64 tipCap int64
feeCap int64 feeCap int64
@ -652,8 +650,26 @@ func TestEffectiveGasTipInto(t *testing.T) {
{tipCap: 50, feeCap: 100, baseFee: nil}, // nil base fee {tipCap: 50, feeCap: 100, baseFee: nil}, // nil base fee
} }
// original, non-allocation golfed version
orig := func(tx *Transaction, baseFee *big.Int) (*big.Int, error) {
if baseFee == nil {
return tx.GasTipCap(), nil
}
var err error
gasFeeCap := tx.GasFeeCap()
if gasFeeCap.Cmp(baseFee) < 0 {
err = ErrGasFeeCapTooLow
}
gasFeeCap = gasFeeCap.Sub(gasFeeCap, baseFee)
gasTipCap := tx.GasTipCap()
if gasTipCap.Cmp(gasFeeCap) < 0 {
return gasTipCap, err
}
return gasFeeCap, err
}
for i, tc := range testCases { for i, tc := range testCases {
txdata := &DynamicFeeTx{ tx := NewTx(&DynamicFeeTx{
ChainID: big.NewInt(1), ChainID: big.NewInt(1),
Nonce: 0, Nonce: 0,
GasTipCap: big.NewInt(tc.tipCap), GasTipCap: big.NewInt(tc.tipCap),
@ -662,27 +678,28 @@ func TestEffectiveGasTipInto(t *testing.T) {
To: &common.Address{}, To: &common.Address{},
Value: big.NewInt(0), Value: big.NewInt(0),
Data: nil, Data: nil,
} })
tx, _ := SignNewTx(key, signer, txdata)
var baseFee *big.Int var baseFee *big.Int
var baseFee2 *uint256.Int
if tc.baseFee != nil { if tc.baseFee != nil {
baseFee = big.NewInt(*tc.baseFee) baseFee = big.NewInt(*tc.baseFee)
baseFee2 = uint256.NewInt(uint64(*tc.baseFee))
} }
// Get result from original method // Get result from original method
orig, origErr := tx.EffectiveGasTip(baseFee) orig, origErr := orig(tx, baseFee)
// Get result from new method // Get result from new method
dst := new(big.Int) dst := new(uint256.Int)
newErr := tx.calcEffectiveGasTip(dst, baseFee) newErr := tx.calcEffectiveGasTip(dst, baseFee2)
// Compare results // Compare results
if (origErr != nil) != (newErr != nil) { if (origErr != nil) != (newErr != nil) {
t.Fatalf("case %d: error mismatch: orig %v, new %v", i, origErr, newErr) t.Fatalf("case %d: error mismatch: orig %v, new %v", i, origErr, newErr)
} }
if orig.Cmp(dst) != 0 { if origErr == nil && orig.Cmp(dst.ToBig()) != 0 {
t.Fatalf("case %d: result mismatch: orig %v, new %v", i, orig, dst) t.Fatalf("case %d: result mismatch: orig %v, new %v", i, orig, dst)
} }
} }
@ -692,3 +709,28 @@ func TestEffectiveGasTipInto(t *testing.T) {
func intPtr(i int64) *int64 { func intPtr(i int64) *int64 {
return &i return &i
} }
func BenchmarkEffectiveGasTipCmp(b *testing.B) {
signer := LatestSigner(params.TestChainConfig)
key, _ := crypto.GenerateKey()
txdata := &DynamicFeeTx{
ChainID: big.NewInt(1),
Nonce: 0,
GasTipCap: big.NewInt(2000000000),
GasFeeCap: big.NewInt(3000000000),
Gas: 21000,
To: &common.Address{},
Value: big.NewInt(0),
Data: nil,
}
tx, _ := SignNewTx(key, signer, txdata)
other, _ := SignNewTx(key, signer, txdata)
baseFee := uint256.NewInt(1000000000) // 1 gwei
b.Run("Original", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
tx.EffectiveGasTipCmp(other, baseFee)
}
})
}