From 8b364cd19527fffad7edc3ad0073d7732800d57d Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Fri, 27 May 2022 22:14:03 -0500 Subject: [PATCH] blockchain: Use new uint256 for work sums. Live profiling data of performing an initial sync shows that roughly 36% of all in-use allocations are the result of the big integers used to store the cumulative work for each block. Further, around 12% of the entire CPU time is spent scaning the heap for garbage collection which is a direct result of the total number of inuse allocations. Therefore, a reasonable expectation is that eliminating those heap objects should produce a speedup of around 4-5%. Consequently, this modifies the blockchain package to make use of the much more efficient zero-alloc uint256s and associated work calculation funcs in the new primitives package that is under development. Profiling shows the result is about 100MiB less heap usage on average and a reduction of about 5% to the initial sync time which is in line with the expected result. --- internal/blockchain/blockindex.go | 16 ++++----- internal/blockchain/blockindex_test.go | 46 +++++++++++++------------- internal/blockchain/chain.go | 21 +++++++++--- internal/blockchain/chainio.go | 32 +++++++++++++----- internal/blockchain/chainio_test.go | 20 ++++++----- internal/blockchain/process.go | 8 ++--- 6 files changed, 86 insertions(+), 57 deletions(-) diff --git a/internal/blockchain/blockindex.go b/internal/blockchain/blockindex.go index 9d7c76b511..17bb9341d5 100644 --- a/internal/blockchain/blockindex.go +++ b/internal/blockchain/blockindex.go @@ -8,15 +8,15 @@ package blockchain import ( "bytes" "encoding/binary" - "math/big" "sort" "sync" "time" "github.com/decred/dcrd/blockchain/stake/v5" - "github.com/decred/dcrd/blockchain/standalone/v2" "github.com/decred/dcrd/chaincfg/chainhash" "github.com/decred/dcrd/database/v3" + "github.com/decred/dcrd/internal/staging/primitives" + "github.com/decred/dcrd/math/uint256" "github.com/decred/dcrd/wire" ) @@ -128,7 +128,7 @@ type blockNode struct { // workSum is the total amount of work in the chain up to and including // this node. - workSum *big.Int + workSum uint256.Uint256 // Some fields from block headers to aid in best chain selection and // reconstructing headers from memory. These must be treated as @@ -233,7 +233,7 @@ func calcSkipListHeight(height int64) int64 { func initBlockNode(node *blockNode, blockHeader *wire.BlockHeader, parent *blockNode) { *node = blockNode{ hash: blockHeader.BlockHash(), - workSum: standalone.CalcWork(blockHeader.Bits), + workSum: primitives.CalcWork(blockHeader.Bits), height: int64(blockHeader.Height), blockVersion: blockHeader.Version, voteBits: blockHeader.VoteBits, @@ -256,7 +256,7 @@ func initBlockNode(node *blockNode, blockHeader *wire.BlockHeader, parent *block if parent != nil { node.parent = parent node.skipToAncestor = parent.Ancestor(calcSkipListHeight(node.height)) - node.workSum = node.workSum.Add(parent.workSum, node.workSum) + node.workSum.Add(&parent.workSum) } } @@ -448,7 +448,7 @@ func betterCandidate(a, b *blockNode) bool { // // Blocks with more cumulative work are better candidates for best chain // selection. - if workCmp := a.workSum.Cmp(b.workSum); workCmp != 0 { + if workCmp := a.workSum.Cmp(&b.workSum); workCmp != 0 { return workCmp > 0 } @@ -1249,7 +1249,7 @@ func (bi *blockIndex) removeLessWorkCandidates(node *blockNode) { // Remove all best chain candidates that have less work than the passed // node. for n := range bi.bestChainCandidates { - if n.workSum.Cmp(node.workSum) < 0 { + if n.workSum.Lt(&node.workSum) { bi.removeBestChainCandidate(n) } } @@ -1306,7 +1306,7 @@ func (bi *blockIndex) linkBlockData(node, tip *blockNode) []*blockNode { // The block is now a candidate to potentially become the best chain if // it has the same or more work than the current best chain tip. - if linkedNode.workSum.Cmp(tip.workSum) >= 0 { + if linkedNode.workSum.GtEq(&tip.workSum) { bi.addBestChainCandidate(linkedNode) } diff --git a/internal/blockchain/blockindex_test.go b/internal/blockchain/blockindex_test.go index 46ae770cac..a10cb6e1cc 100644 --- a/internal/blockchain/blockindex_test.go +++ b/internal/blockchain/blockindex_test.go @@ -6,13 +6,13 @@ package blockchain import ( "fmt" - "math/big" "math/rand" "reflect" "testing" "time" "github.com/decred/dcrd/chaincfg/v3" + "github.com/decred/dcrd/math/uint256" "github.com/decred/dcrd/wire" ) @@ -406,13 +406,13 @@ func TestBetterCandidate(t *testing.T) { name: "exactly equal, both data", nodeA: &blockNode{ hash: *mustParseHash("0000000000000000000000000000000000000000000000000000000000000000"), - workSum: big.NewInt(2), + workSum: *new(uint256.Uint256).SetUint64(2), status: statusDataStored, receivedOrderID: 0, }, nodeB: &blockNode{ hash: *mustParseHash("0000000000000000000000000000000000000000000000000000000000000000"), - workSum: big.NewInt(2), + workSum: *new(uint256.Uint256).SetUint64(2), status: statusDataStored, receivedOrderID: 0, }, @@ -422,12 +422,12 @@ func TestBetterCandidate(t *testing.T) { name: "exactly equal, no data", nodeA: &blockNode{ hash: *mustParseHash("0000000000000000000000000000000000000000000000000000000000000000"), - workSum: big.NewInt(2), + workSum: *new(uint256.Uint256).SetUint64(2), receivedOrderID: 0, }, nodeB: &blockNode{ hash: *mustParseHash("0000000000000000000000000000000000000000000000000000000000000000"), - workSum: big.NewInt(2), + workSum: *new(uint256.Uint256).SetUint64(2), receivedOrderID: 0, }, wantCmp: 0, @@ -436,12 +436,12 @@ func TestBetterCandidate(t *testing.T) { name: "a has more cumulative work, same order, higher hash, b has data", nodeA: &blockNode{ hash: *higherHash, - workSum: big.NewInt(4), + workSum: *new(uint256.Uint256).SetUint64(4), receivedOrderID: 0, }, nodeB: &blockNode{ hash: *lowerHash, - workSum: big.NewInt(2), + workSum: *new(uint256.Uint256).SetUint64(2), status: statusDataStored, receivedOrderID: 0, }, @@ -451,13 +451,13 @@ func TestBetterCandidate(t *testing.T) { name: "a has less cumulative work, same order, lower hash, a has data", nodeA: &blockNode{ hash: *lowerHash, - workSum: big.NewInt(2), + workSum: *new(uint256.Uint256).SetUint64(2), status: statusDataStored, receivedOrderID: 0, }, nodeB: &blockNode{ hash: *higherHash, - workSum: big.NewInt(4), + workSum: *new(uint256.Uint256).SetUint64(4), receivedOrderID: 0, }, wantCmp: -1, @@ -466,12 +466,12 @@ func TestBetterCandidate(t *testing.T) { name: "a has same cumulative work, same order, lower hash, b has data", nodeA: &blockNode{ hash: *lowerHash, - workSum: big.NewInt(2), + workSum: *new(uint256.Uint256).SetUint64(2), receivedOrderID: 0, }, nodeB: &blockNode{ hash: *higherHash, - workSum: big.NewInt(2), + workSum: *new(uint256.Uint256).SetUint64(2), status: statusDataStored, receivedOrderID: 0, }, @@ -481,13 +481,13 @@ func TestBetterCandidate(t *testing.T) { name: "a has same cumulative work, same order, higher hash, a has data", nodeA: &blockNode{ hash: *higherHash, - workSum: big.NewInt(2), + workSum: *new(uint256.Uint256).SetUint64(2), status: statusDataStored, receivedOrderID: 0, }, nodeB: &blockNode{ hash: *lowerHash, - workSum: big.NewInt(2), + workSum: *new(uint256.Uint256).SetUint64(2), receivedOrderID: 0, }, wantCmp: 1, @@ -496,13 +496,13 @@ func TestBetterCandidate(t *testing.T) { name: "a has same cumulative work, higher order, lower hash, both data", nodeA: &blockNode{ hash: *lowerHash, - workSum: big.NewInt(2), + workSum: *new(uint256.Uint256).SetUint64(2), status: statusDataStored, receivedOrderID: 1, }, nodeB: &blockNode{ hash: *higherHash, - workSum: big.NewInt(2), + workSum: *new(uint256.Uint256).SetUint64(2), status: statusDataStored, receivedOrderID: 0, }, @@ -512,13 +512,13 @@ func TestBetterCandidate(t *testing.T) { name: "a has same cumulative work, lower order, lower hash, both data", nodeA: &blockNode{ hash: *lowerHash, - workSum: big.NewInt(2), + workSum: *new(uint256.Uint256).SetUint64(2), status: statusDataStored, receivedOrderID: 1, }, nodeB: &blockNode{ hash: *higherHash, - workSum: big.NewInt(2), + workSum: *new(uint256.Uint256).SetUint64(2), status: statusDataStored, receivedOrderID: 2, }, @@ -528,12 +528,12 @@ func TestBetterCandidate(t *testing.T) { name: "a has same cumulative work, same order, lower hash, no data", nodeA: &blockNode{ hash: *lowerHash, - workSum: big.NewInt(2), + workSum: *new(uint256.Uint256).SetUint64(2), receivedOrderID: 0, }, nodeB: &blockNode{ hash: *higherHash, - workSum: big.NewInt(2), + workSum: *new(uint256.Uint256).SetUint64(2), receivedOrderID: 0, }, wantCmp: -1, @@ -542,13 +542,13 @@ func TestBetterCandidate(t *testing.T) { name: "a has same cumulative work, same order, lower hash, both data", nodeA: &blockNode{ hash: *lowerHash, - workSum: big.NewInt(2), + workSum: *new(uint256.Uint256).SetUint64(2), status: statusDataStored, receivedOrderID: 0, }, nodeB: &blockNode{ hash: *higherHash, - workSum: big.NewInt(2), + workSum: *new(uint256.Uint256).SetUint64(2), status: statusDataStored, receivedOrderID: 0, }, @@ -558,13 +558,13 @@ func TestBetterCandidate(t *testing.T) { name: "a has same cumulative work, same order, higher hash, both data", nodeA: &blockNode{ hash: *higherHash, - workSum: big.NewInt(2), + workSum: *new(uint256.Uint256).SetUint64(2), status: statusDataStored, receivedOrderID: 0, }, nodeB: &blockNode{ hash: *lowerHash, - workSum: big.NewInt(2), + workSum: *new(uint256.Uint256).SetUint64(2), status: statusDataStored, receivedOrderID: 0, }, diff --git a/internal/blockchain/chain.go b/internal/blockchain/chain.go index 6098873239..e20d134a0e 100644 --- a/internal/blockchain/chain.go +++ b/internal/blockchain/chain.go @@ -26,6 +26,7 @@ import ( "github.com/decred/dcrd/internal/blockchain/indexers" "github.com/decred/dcrd/internal/blockchain/spendpruner" "github.com/decred/dcrd/lru" + "github.com/decred/dcrd/math/uint256" "github.com/decred/dcrd/txscript/v4" "github.com/decred/dcrd/wire" ) @@ -141,6 +142,7 @@ type BlockChain struct { allowOldForks bool expectedBlocksInTwoWeeks int64 deploymentVers map[string]uint32 + minKnownWork *uint256.Uint256 db database.DB dbInfo *databaseInfo chainParams *chaincfg.Params @@ -467,7 +469,7 @@ func (b *BlockChain) ChainWork(hash *chainhash.Hash) (*big.Int, error) { return nil, unknownBlockError(hash) } - return node.workSum, nil + return node.workSum.ToBig(), nil } // TipGeneration returns the entire generation of blocks stemming from the @@ -710,7 +712,7 @@ func (b *BlockChain) connectBlock(node *blockNode, block, parent *dcrutil.Block, // Atomically insert info into the database. err = b.db.Update(func(dbTx database.Tx) error { // Update best block state. - err := dbPutBestState(dbTx, state, node.workSum) + err := dbPutBestState(dbTx, state, &node.workSum) if err != nil { return err } @@ -922,7 +924,7 @@ func (b *BlockChain) disconnectBlock(node *blockNode, block, parent *dcrutil.Blo err = b.db.Update(func(dbTx database.Tx) error { // Update best block state. - err := dbPutBestState(dbTx, state, node.workSum) + err := dbPutBestState(dbTx, state, &node.workSum) if err != nil { return err } @@ -1593,8 +1595,7 @@ func (b *BlockChain) maybeUpdateIsCurrent(curBest *blockNode) { if !b.isCurrentLatch { // Not current if the latest best block has a cumulative work less than // the minimum known work specified by the network parameters. - minKnownWork := b.chainParams.MinKnownChainWork - if minKnownWork != nil && curBest.workSum.Cmp(minKnownWork) < 0 { + if b.minKnownWork != nil && curBest.workSum.Lt(b.minKnownWork) { return } @@ -2371,6 +2372,15 @@ func New(ctx context.Context, config *Config) (*BlockChain, error) { return nil, err } + // Convert the minimum known work to a uint256 when it it exists. Ideally, + // the chain params should be updated to use the new type, but that will be + // a major version bump, so a one-time conversion is a good tradeoff in the + // mean time. + var minKnownWork *uint256.Uint256 + if params.MinKnownChainWork != nil { + minKnownWork = new(uint256.Uint256).SetBig(params.MinKnownChainWork) + } + // Either use the subsidy cache provided by the caller or create a new // one when one was not provided. subsidyCache := config.SubsidyCache @@ -2396,6 +2406,7 @@ func New(ctx context.Context, config *Config) (*BlockChain, error) { allowOldForks: allowOldForks, expectedBlocksInTwoWeeks: expectedBlksInTwoWeeks, deploymentVers: deploymentVers, + minKnownWork: minKnownWork, db: config.DB, chainParams: params, timeSource: config.TimeSource, diff --git a/internal/blockchain/chainio.go b/internal/blockchain/chainio.go index 5d964f394b..a31fbc81e6 100644 --- a/internal/blockchain/chainio.go +++ b/internal/blockchain/chainio.go @@ -11,7 +11,6 @@ import ( "encoding/binary" "errors" "fmt" - "math/big" "time" "github.com/decred/dcrd/blockchain/stake/v5" @@ -21,6 +20,7 @@ import ( "github.com/decred/dcrd/dcrutil/v4" "github.com/decred/dcrd/gcs/v4" "github.com/decred/dcrd/gcs/v4/blockcf2" + "github.com/decred/dcrd/math/uint256" "github.com/decred/dcrd/wire" ) @@ -1120,7 +1120,7 @@ func dbFetchDatabaseInfo(dbTx database.Tx) *databaseInfo { // total txns uint64 8 bytes // total subsidy int64 8 bytes // work sum length uint32 4 bytes -// work sum big.Int work sum length +// work sum uint256 work sum length // ----------------------------------------------------------------------------- // bestChainState represents the data to be stored the database for the current @@ -1130,14 +1130,30 @@ type bestChainState struct { height uint32 totalTxns uint64 totalSubsidy int64 - workSum *big.Int + workSum uint256.Uint256 } // serializeBestChainState returns the serialization of the passed block best // chain state. This is data to be stored in the chain state bucket. func serializeBestChainState(state bestChainState) []byte { // Calculate the full size needed to serialize the chain state. - workSumBytes := state.workSum.Bytes() + // + // NOTE: The leading zero bytes are truncated in order to match the + // semantics of legacy code that made use of stdlib big.Ints which returns a + // variable number of bytes versus the new uint256 type that returns a + // fixed-size array. This is needed because the bytes are stored in the + // database and changing the format would require a version bump and the + // associated migration which does not seem worth it given the difference is + // easily handled efficiently as implemented here. + workSumBytesArray := state.workSum.Bytes() + var firstNonzero int + for i, b := range workSumBytesArray { + if b != 0 { + firstNonzero = i + break + } + } + workSumBytes := workSumBytesArray[firstNonzero:] workSumBytesLen := uint32(len(workSumBytes)) serializedLen := chainhash.HashSize + 4 + 8 + 8 + 4 + workSumBytesLen @@ -1196,21 +1212,21 @@ func deserializeBestChainState(serializedData []byte) (bestChainState, error) { return bestChainState{}, makeDbErr(database.ErrCorruption, str) } workSumBytes := serializedData[offset : offset+workSumBytesLen] - state.workSum = new(big.Int).SetBytes(workSumBytes) + state.workSum = *new(uint256.Uint256).SetByteSlice(workSumBytes) return state, nil } // dbPutBestState uses an existing database transaction to update the best chain // state with the given parameters. -func dbPutBestState(dbTx database.Tx, snapshot *BestState, workSum *big.Int) error { +func dbPutBestState(dbTx database.Tx, snapshot *BestState, workSum *uint256.Uint256) error { // Serialize the current best chain state. serializedData := serializeBestChainState(bestChainState{ hash: snapshot.Hash, height: uint32(snapshot.Height), totalTxns: snapshot.TotalTxns, totalSubsidy: snapshot.TotalSubsidy, - workSum: workSum, + workSum: *workSum, }) // Store the current best chain state into the database. @@ -1308,7 +1324,7 @@ func (b *BlockChain) createChainState() error { } // Store the current best chain state into the database. - err = dbPutBestState(dbTx, stateSnapshot, node.workSum) + err = dbPutBestState(dbTx, stateSnapshot, &node.workSum) if err != nil { return err } diff --git a/internal/blockchain/chainio_test.go b/internal/blockchain/chainio_test.go index 103755e472..8793f3f28c 100644 --- a/internal/blockchain/chainio_test.go +++ b/internal/blockchain/chainio_test.go @@ -10,16 +10,16 @@ import ( "encoding/hex" "errors" "math" - "math/big" "reflect" "testing" "time" "github.com/decred/dcrd/blockchain/stake/v5" - "github.com/decred/dcrd/blockchain/standalone/v2" "github.com/decred/dcrd/chaincfg/chainhash" "github.com/decred/dcrd/chaincfg/v3" "github.com/decred/dcrd/database/v3" + "github.com/decred/dcrd/internal/staging/primitives" + "github.com/decred/dcrd/math/uint256" "github.com/decred/dcrd/wire" ) @@ -892,7 +892,7 @@ func TestHeaderCommitmentDeserializeErrors(t *testing.T) { func TestBestChainStateSerialization(t *testing.T) { t.Parallel() - workSum := new(big.Int) + var workSum uint256.Uint256 tests := []struct { name string state bestChainState @@ -905,9 +905,10 @@ func TestBestChainStateSerialization(t *testing.T) { height: 0, totalTxns: 1, totalSubsidy: 0, - workSum: func() *big.Int { - workSum.Add(workSum, standalone.CalcWork(486604799)) - return new(big.Int).Set(workSum) + workSum: func() uint256.Uint256 { + work := primitives.CalcWork(486604799) + workSum.Add(&work) + return workSum }(), // 0x0100010001 }, serialized: hexToBytes("6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d61900000000000000000001000000000000000000000000000000050000000100010001"), @@ -919,9 +920,10 @@ func TestBestChainStateSerialization(t *testing.T) { height: 1, totalTxns: 2, totalSubsidy: 123456789, - workSum: func() *big.Int { - workSum.Add(workSum, standalone.CalcWork(486604799)) - return new(big.Int).Set(workSum) + workSum: func() uint256.Uint256 { + work := primitives.CalcWork(486604799) + workSum.Add(&work) + return workSum }(), // 0x0200020002, }, serialized: hexToBytes("4860eb18bf1b1620e37e9490fc8a427514416fd75159ab86688e9a830000000001000000020000000000000015cd5b0700000000050000000200020002"), diff --git a/internal/blockchain/process.go b/internal/blockchain/process.go index c209e6c757..a0da54ae0b 100644 --- a/internal/blockchain/process.go +++ b/internal/blockchain/process.go @@ -736,7 +736,7 @@ func (b *BlockChain) InvalidateBlock(hash *chainhash.Hash) error { // Chain tips that have less work than the new tip are not best chain // candidates nor are any of their ancestors since they have even less // work. - if tip.workSum.Cmp(newTip.workSum) < 0 { + if tip.workSum.Lt(&newTip.workSum) { return nil } @@ -748,7 +748,7 @@ func (b *BlockChain) InvalidateBlock(hash *chainhash.Hash) error { for n != nil && (n.status.KnownInvalid() || !b.index.canValidate(n)) { n = n.parent } - if n != nil && n != newTip && n.workSum.Cmp(newTip.workSum) >= 0 { + if n != nil && n != newTip && n.workSum.GtEq(&newTip.workSum) { b.index.addBestChainCandidate(n) } @@ -826,7 +826,7 @@ func (b *BlockChain) ReconsiderBlock(hash *chainhash.Hash) error { b.recentContextChecks.Delete(n.hash) } - if b.index.canValidate(n) && n.workSum.Cmp(curBestTip.workSum) >= 0 { + if b.index.canValidate(n) && n.workSum.GtEq(&curBestTip.workSum) { b.index.addBestChainCandidate(n) } @@ -875,7 +875,7 @@ func (b *BlockChain) ReconsiderBlock(hash *chainhash.Hash) error { for n := finalNotKnownInvalidDescendant; n != vfNode; n = n.parent { b.index.unsetStatusFlags(n, statusInvalidAncestor) b.recentContextChecks.Delete(n.hash) - if b.index.canValidate(n) && n.workSum.Cmp(curBestTip.workSum) >= 0 { + if b.index.canValidate(n) && n.workSum.GtEq(&curBestTip.workSum) { b.index.addBestChainCandidate(n) }