Skip to content

Commit befbc6f

Browse files
MariusVanDerWijdenholiman
authored andcommitted
core, txpool: less allocations when handling transactions (ethereum#21232)
* core: use uint64 for total tx costs instead of big.Int * core: added local tx pool test case * core, crypto: various allocation savings regarding tx handling * Update core/tx_list.go * core: added tx.GasPriceIntCmp for comparison without allocation adds a method to remove unneeded allocation in comparison to tx.gasPrice * core: handle pools full of locals better * core/tests: benchmark for tx_list * core/txlist, txpool: save a reheap operation, avoid some bigint allocs Co-authored-by: Martin Holst Swende <[email protected]>
1 parent e1c7147 commit befbc6f

File tree

6 files changed

+151
-54
lines changed

6 files changed

+151
-54
lines changed

common/math/integer.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package math
1818

1919
import (
2020
"fmt"
21+
"math/bits"
2122
"strconv"
2223
)
2324

@@ -87,13 +88,12 @@ func SafeSub(x, y uint64) (uint64, bool) {
8788

8889
// SafeAdd returns the result and whether overflow occurred.
8990
func SafeAdd(x, y uint64) (uint64, bool) {
90-
return x + y, y > MaxUint64-x
91+
sum, carry := bits.Add64(x, y, 0)
92+
return sum, carry != 0
9193
}
9294

9395
// SafeMul returns multiplication result and whether overflow occurred.
9496
func SafeMul(x, y uint64) (uint64, bool) {
95-
if x == 0 || y == 0 {
96-
return 0, false
97-
}
98-
return x * y, y > MaxUint64/x
97+
hi, lo := bits.Mul64(x, y)
98+
return lo, hi != 0
9999
}

core/tx_list.go

Lines changed: 99 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,30 @@ func (m *txSortedMap) Forward(threshold uint64) types.Transactions {
9999

100100
// Filter iterates over the list of transactions and removes all of them for which
101101
// the specified function evaluates to true.
102+
// Filter, as opposed to 'filter', re-initialises the heap after the operation is done.
103+
// If you want to do several consecutive filterings, it's therefore better to first
104+
// do a .filter(func1) followed by .Filter(func2) or reheap()
102105
func (m *txSortedMap) Filter(filter func(*types.Transaction) bool) types.Transactions {
106+
removed := m.filter(filter)
107+
// If transactions were removed, the heap and cache are ruined
108+
if len(removed) > 0 {
109+
m.reheap()
110+
}
111+
return removed
112+
}
113+
114+
func (m *txSortedMap) reheap() {
115+
*m.index = make([]uint64, 0, len(m.items))
116+
for nonce := range m.items {
117+
*m.index = append(*m.index, nonce)
118+
}
119+
heap.Init(m.index)
120+
m.cache = nil
121+
}
122+
123+
// filter is identical to Filter, but **does not** regenerate the heap. This method
124+
// should only be used if followed immediately by a call to Filter or reheap()
125+
func (m *txSortedMap) filter(filter func(*types.Transaction) bool) types.Transactions {
103126
var removed types.Transactions
104127

105128
// Collect all the transactions to filter out
@@ -109,14 +132,7 @@ func (m *txSortedMap) Filter(filter func(*types.Transaction) bool) types.Transac
109132
delete(m.items, nonce)
110133
}
111134
}
112-
// If transactions were removed, the heap and cache are ruined
113135
if len(removed) > 0 {
114-
*m.index = make([]uint64, 0, len(m.items))
115-
for nonce := range m.items {
116-
*m.index = append(*m.index, nonce)
117-
}
118-
heap.Init(m.index)
119-
120136
m.cache = nil
121137
}
122138
return removed
@@ -197,10 +213,7 @@ func (m *txSortedMap) Len() int {
197213
return len(m.items)
198214
}
199215

200-
// Flatten creates a nonce-sorted slice of transactions based on the loosely
201-
// sorted internal representation. The result of the sorting is cached in case
202-
// it's requested again before any modifications are made to the contents.
203-
func (m *txSortedMap) Flatten() types.Transactions {
216+
func (m *txSortedMap) flatten() types.Transactions {
204217
// If the sorting was not cached yet, create and cache it
205218
if m.cache == nil {
206219
m.cache = make(types.Transactions, 0, len(m.items))
@@ -209,12 +222,27 @@ func (m *txSortedMap) Flatten() types.Transactions {
209222
}
210223
sort.Sort(types.TxByNonce(m.cache))
211224
}
225+
return m.cache
226+
}
227+
228+
// Flatten creates a nonce-sorted slice of transactions based on the loosely
229+
// sorted internal representation. The result of the sorting is cached in case
230+
// it's requested again before any modifications are made to the contents.
231+
func (m *txSortedMap) Flatten() types.Transactions {
212232
// Copy the cache to prevent accidental modifications
213-
txs := make(types.Transactions, len(m.cache))
214-
copy(txs, m.cache)
233+
cache := m.flatten()
234+
txs := make(types.Transactions, len(cache))
235+
copy(txs, cache)
215236
return txs
216237
}
217238

239+
// LastElement returns the last element of a flattened list, thus, the
240+
// transaction with the highest nonce
241+
func (m *txSortedMap) LastElement() *types.Transaction {
242+
cache := m.flatten()
243+
return cache[len(cache)-1]
244+
}
245+
218246
// txList is a "list" of transactions belonging to an account, sorted by account
219247
// nonce. The same type can be used both for storing contiguous transactions for
220248
// the executable/pending queue; and for storing gapped transactions for the non-
@@ -223,17 +251,16 @@ type txList struct {
223251
strict bool // Whether nonces are strictly continuous or not
224252
txs *txSortedMap // Heap indexed sorted hash map of the transactions
225253

226-
costcap *big.Int // Price of the highest costing transaction (reset only if exceeds balance)
227-
gascap uint64 // Gas limit of the highest spending transaction (reset only if exceeds block limit)
254+
costcap uint64 // Price of the highest costing transaction (reset only if exceeds balance)
255+
gascap uint64 // Gas limit of the highest spending transaction (reset only if exceeds block limit)
228256
}
229257

230258
// newTxList create a new transaction list for maintaining nonce-indexable fast,
231259
// gapped, sortable transaction lists.
232260
func newTxList(strict bool) *txList {
233261
return &txList{
234-
strict: strict,
235-
txs: newTxSortedMap(),
236-
costcap: new(big.Int),
262+
strict: strict,
263+
txs: newTxSortedMap(),
237264
}
238265
}
239266

@@ -252,17 +279,26 @@ func (l *txList) Add(tx *types.Transaction, priceBump uint64) (bool, *types.Tran
252279
// If there's an older better transaction, abort
253280
old := l.txs.Get(tx.Nonce())
254281
if old != nil {
255-
threshold := new(big.Int).Div(new(big.Int).Mul(old.GasPrice(), big.NewInt(100+int64(priceBump))), big.NewInt(100))
282+
// threshold = oldGP * (100 + priceBump) / 100
283+
a := big.NewInt(100 + int64(priceBump))
284+
a = a.Mul(a, old.GasPrice())
285+
b := big.NewInt(100)
286+
threshold := a.Div(a, b)
256287
// Have to ensure that the new gas price is higher than the old gas
257288
// price as well as checking the percentage threshold to ensure that
258289
// this is accurate for low (Wei-level) gas price replacements
259290
if old.GasPriceCmp(tx) >= 0 || tx.GasPriceIntCmp(threshold) < 0 {
260291
return false, nil
261292
}
262293
}
294+
cost, overflow := tx.CostU64()
295+
if overflow {
296+
log.Warn("transaction cost overflown, txHash: %v txCost: %v", tx.Hash(), cost)
297+
return false, nil
298+
}
263299
// Otherwise overwrite the old transaction with the current one
264300
l.txs.Put(tx)
265-
if cost := tx.Cost(); l.costcap.Cmp(cost) < 0 {
301+
if l.costcap < cost {
266302
l.costcap = cost
267303
}
268304
if gas := tx.Gas(); l.gascap < gas {
@@ -287,29 +323,35 @@ func (l *txList) Forward(threshold uint64) types.Transactions {
287323
// a point in calculating all the costs or if the balance covers all. If the threshold
288324
// is lower than the costgas cap, the caps will be reset to a new high after removing
289325
// the newly invalidated transactions.
290-
func (l *txList) Filter(costLimit *big.Int, gasLimit uint64) (types.Transactions, types.Transactions) {
326+
func (l *txList) Filter(costLimit uint64, gasLimit uint64) (types.Transactions, types.Transactions) {
291327
// If all transactions are below the threshold, short circuit
292-
if l.costcap.Cmp(costLimit) <= 0 && l.gascap <= gasLimit {
328+
if l.costcap <= costLimit && l.gascap <= gasLimit {
293329
return nil, nil
294330
}
295-
l.costcap = new(big.Int).Set(costLimit) // Lower the caps to the thresholds
331+
l.costcap = costLimit // Lower the caps to the thresholds
296332
l.gascap = gasLimit
297333

298334
// Filter out all the transactions above the account's funds
299-
removed := l.txs.Filter(func(tx *types.Transaction) bool { return tx.Cost().Cmp(costLimit) > 0 || tx.Gas() > gasLimit })
335+
removed := l.txs.filter(func(tx *types.Transaction) bool {
336+
cost, _ := tx.CostU64()
337+
return cost > costLimit || tx.Gas() > gasLimit
338+
})
300339

301-
// If the list was strict, filter anything above the lowest nonce
340+
if len(removed) == 0 {
341+
return nil, nil
342+
}
302343
var invalids types.Transactions
303-
304-
if l.strict && len(removed) > 0 {
344+
// If the list was strict, filter anything above the lowest nonce
345+
if l.strict {
305346
lowest := uint64(math.MaxUint64)
306347
for _, tx := range removed {
307348
if nonce := tx.Nonce(); lowest > nonce {
308349
lowest = nonce
309350
}
310351
}
311-
invalids = l.txs.Filter(func(tx *types.Transaction) bool { return tx.Nonce() > lowest })
352+
invalids = l.txs.filter(func(tx *types.Transaction) bool { return tx.Nonce() > lowest })
312353
}
354+
l.txs.reheap()
313355
return removed, invalids
314356
}
315357

@@ -363,6 +405,12 @@ func (l *txList) Flatten() types.Transactions {
363405
return l.txs.Flatten()
364406
}
365407

408+
// LastElement returns the last element of a flattened list, thus, the
409+
// transaction with the highest nonce
410+
func (l *txList) LastElement() *types.Transaction {
411+
return l.txs.LastElement()
412+
}
413+
366414
// priceHeap is a heap.Interface implementation over transactions for retrieving
367415
// price-sorted transactions to discard when the pool fills up.
368416
type priceHeap []*types.Transaction
@@ -495,8 +543,29 @@ func (l *txPricedList) Underpriced(tx *types.Transaction, local *accountSet) boo
495543
// Discard finds a number of most underpriced transactions, removes them from the
496544
// priced list and returns them for further removal from the entire pool.
497545
func (l *txPricedList) Discard(slots int, local *accountSet) types.Transactions {
498-
drop := make(types.Transactions, 0, slots) // Remote underpriced transactions to drop
499-
save := make(types.Transactions, 0, 64) // Local underpriced transactions to keep
546+
// If we have some local accountset, those will not be discarded
547+
if !local.empty() {
548+
// In case the list is filled to the brim with 'local' txs, we do this
549+
// little check to avoid unpacking / repacking the heap later on, which
550+
// is very expensive
551+
discardable := 0
552+
for _, tx := range *l.items {
553+
if !local.containsTx(tx) {
554+
discardable++
555+
}
556+
if discardable >= slots {
557+
break
558+
}
559+
}
560+
if slots > discardable {
561+
slots = discardable
562+
}
563+
}
564+
if slots == 0 {
565+
return nil
566+
}
567+
drop := make(types.Transactions, 0, slots) // Remote underpriced transactions to drop
568+
save := make(types.Transactions, 0, len(*l.items)-slots) // Local underpriced transactions to keep
500569

501570
for len(*l.items) > 0 && slots > 0 {
502571
// Discard stale transactions if found during cleanup

core/tx_list_test.go

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
package core
1818

1919
import (
20-
"math/big"
2120
"math/rand"
2221
"testing"
2322

@@ -51,20 +50,22 @@ func TestStrictTxListAdd(t *testing.T) {
5150
}
5251
}
5352

54-
func BenchmarkTxListAdd(t *testing.B) {
53+
func BenchmarkTxListAdd(b *testing.B) {
5554
// Generate a list of transactions to insert
5655
key, _ := crypto.GenerateKey()
5756

58-
txs := make(types.Transactions, 100000)
57+
txs := make(types.Transactions, 2000)
5958
for i := 0; i < len(txs); i++ {
6059
txs[i] = transaction(uint64(i), 0, key)
6160
}
6261
// Insert the transactions in a random order
63-
list := newTxList(true)
64-
priceLimit := big.NewInt(int64(DefaultTxPoolConfig.PriceLimit))
65-
t.ResetTimer()
66-
for _, v := range rand.Perm(len(txs)) {
67-
list.Add(txs[v], DefaultTxPoolConfig.PriceBump)
68-
list.Filter(priceLimit, DefaultTxPoolConfig.PriceBump)
62+
b.ResetTimer()
63+
priceLimit := DefaultTxPoolConfig.PriceLimit
64+
for i := 0; i < b.N; i++ {
65+
list := newTxList(true)
66+
for _, v := range rand.Perm(len(txs)) {
67+
list.Add(txs[v], DefaultTxPoolConfig.PriceBump)
68+
list.Filter(priceLimit, DefaultTxPoolConfig.PriceBump)
69+
}
6970
}
7071
}

core/tx_pool.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -543,7 +543,11 @@ func (pool *TxPool) validateTx(tx *types.Transaction, local bool) error {
543543
}
544544
// Transactor should have enough funds to cover the costs
545545
// cost == V + GP * GL
546-
if pool.currentState.GetBalance(from).Cmp(tx.Cost()) < 0 {
546+
cost, overflow := tx.CostU64()
547+
if overflow {
548+
return ErrInsufficientFunds
549+
}
550+
if pool.currentState.GetBalance(from).Uint64() < cost {
547551
return ErrInsufficientFunds
548552
}
549553
// Ensure the transaction has more gas than the basic tx fee.
@@ -1059,8 +1063,8 @@ func (pool *TxPool) runReorg(done chan struct{}, reset *txpoolResetRequest, dirt
10591063

10601064
// Update all accounts to the latest known pending nonce
10611065
for addr, list := range pool.pending {
1062-
txs := list.Flatten() // Heavy but will be cached and is needed by the miner anyway
1063-
pool.pendingNonces.set(addr, txs[len(txs)-1].Nonce()+1)
1066+
highestPending := list.LastElement()
1067+
pool.pendingNonces.set(addr, highestPending.Nonce()+1)
10641068
}
10651069
pool.mu.Unlock()
10661070

@@ -1190,7 +1194,7 @@ func (pool *TxPool) promoteExecutables(accounts []common.Address) []*types.Trans
11901194
}
11911195
log.Trace("Removed old queued transactions", "count", len(forwards))
11921196
// Drop all transactions that are too costly (low balance or out of gas)
1193-
drops, _ := list.Filter(pool.currentState.GetBalance(addr), pool.currentMaxGas)
1197+
drops, _ := list.Filter(pool.currentState.GetBalance(addr).Uint64(), pool.currentMaxGas)
11941198
for _, tx := range drops {
11951199
hash := tx.Hash()
11961200
pool.all.Remove(hash)
@@ -1382,7 +1386,7 @@ func (pool *TxPool) demoteUnexecutables() {
13821386
log.Trace("Removed old pending transaction", "hash", hash)
13831387
}
13841388
// Drop all transactions that are too costly (low balance or out of gas), and queue any invalids back for later
1385-
drops, invalids := list.Filter(pool.currentState.GetBalance(addr), pool.currentMaxGas)
1389+
drops, invalids := list.Filter(pool.currentState.GetBalance(addr).Uint64(), pool.currentMaxGas)
13861390
for _, tx := range drops {
13871391
hash := tx.Hash()
13881392
log.Trace("Removed unpayable pending transaction", "hash", hash)
@@ -1457,6 +1461,10 @@ func (as *accountSet) contains(addr common.Address) bool {
14571461
return exist
14581462
}
14591463

1464+
func (as *accountSet) empty() bool {
1465+
return len(as.accounts) == 0
1466+
}
1467+
14601468
// containsTx checks if the sender of a given tx is within the set. If the sender
14611469
// cannot be derived, this method returns false.
14621470
func (as *accountSet) containsTx(tx *types.Transaction) bool {

core/tx_pool_test.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1889,11 +1889,15 @@ func benchmarkFuturePromotion(b *testing.B, size int) {
18891889
}
18901890

18911891
// Benchmarks the speed of batched transaction insertion.
1892-
func BenchmarkPoolBatchInsert100(b *testing.B) { benchmarkPoolBatchInsert(b, 100) }
1893-
func BenchmarkPoolBatchInsert1000(b *testing.B) { benchmarkPoolBatchInsert(b, 1000) }
1894-
func BenchmarkPoolBatchInsert10000(b *testing.B) { benchmarkPoolBatchInsert(b, 10000) }
1892+
func BenchmarkPoolBatchInsert100(b *testing.B) { benchmarkPoolBatchInsert(b, 100, false) }
1893+
func BenchmarkPoolBatchInsert1000(b *testing.B) { benchmarkPoolBatchInsert(b, 1000, false) }
1894+
func BenchmarkPoolBatchInsert10000(b *testing.B) { benchmarkPoolBatchInsert(b, 10000, false) }
18951895

1896-
func benchmarkPoolBatchInsert(b *testing.B, size int) {
1896+
func BenchmarkPoolBatchLocalInsert100(b *testing.B) { benchmarkPoolBatchInsert(b, 100, true) }
1897+
func BenchmarkPoolBatchLocalInsert1000(b *testing.B) { benchmarkPoolBatchInsert(b, 1000, true) }
1898+
func BenchmarkPoolBatchLocalInsert10000(b *testing.B) { benchmarkPoolBatchInsert(b, 10000, true) }
1899+
1900+
func benchmarkPoolBatchInsert(b *testing.B, size int, local bool) {
18971901
// Generate a batch of transactions to enqueue into the pool
18981902
pool, key := setupTxPool()
18991903
defer pool.Stop()
@@ -1911,6 +1915,10 @@ func benchmarkPoolBatchInsert(b *testing.B, size int) {
19111915
// Benchmark importing the transactions into the queue
19121916
b.ResetTimer()
19131917
for _, batch := range batches {
1914-
pool.AddRemotes(batch)
1918+
if local {
1919+
pool.AddLocals(batch)
1920+
} else {
1921+
pool.AddRemotes(batch)
1922+
}
19151923
}
19161924
}

0 commit comments

Comments
 (0)