prevector: store P2WSH/P2TR/P2PK scripts inline

The current `prevector` size of 28 bytes (chosen to fill the `sizeof(CScript)` aligned size) was introduced in 2015 (https://github.com/bitcoin/bitcoin/pull/6914) before SegWit and TapRoot.
However, the increasingly common `P2WSH` and `P2TR` scripts are both 34 bytes, and are forced to use heap (re)allocation rather than efficient inline storage.

The core trade-off of this change is to eliminate heap allocations for common 34-36 byte scripts at the cost of increasing the base memory footprint of all `CScript` objects by 8 bytes (while still respecting peak memory usage defined by `-dbcache`).

Increasing the `prevector` size allows these scripts to be stored inline, avoiding extra heap allocations, reducing potential memory fragmentation, and improving performance during cache flushes. Massif analysis confirms a lower stable memory usage after flushing, suggesting the elimination of heap allocations outweighs the larger base size for common workloads.

Due to memory alignment, increasing the `prevector` size to 36 bytes doesn't change the overall `sizeof(CScript)` compared to an increase to 34 bytes, allowing us to include `P2PK` scripts as well at no additional memory cost.

Performance benchmarks for AssumeUTXO load and flush show:
* Small dbcache (450MB): ~1-3% performance improvement (despite more frequent flushes)
* Large dbcache (4500MB): ~6-8% performance improvement due to fewer heap allocations (and basically the number of flushes)
* Very large dbcache (4500MB): ~5-6% performance improvement due to fewer heap allocations (and memory limit not being reached, so there's no memory penalty)

Full IBD and reindex-chainstate with larger `dbcache` values also show an overall ~2-3% speedup.

Co-authored-by: Ava Chow <github@achow101.com>
Co-authored-by: Andrew Toth <andrewstoth@gmail.com>
This commit is contained in:
Lőrinc 2025-03-27 18:40:08 +01:00
parent ecc6c07e58
commit 3170e2c162
3 changed files with 16 additions and 17 deletions

View file

@ -406,7 +406,7 @@ private:
* Tests in October 2015 showed use of this reduced dbcache memory usage by 23%
* and made an initial sync 13% faster.
*/
static constexpr unsigned int PREVECTOR_SIZE{28};
static constexpr unsigned int PREVECTOR_SIZE{36};
typedef prevector<PREVECTOR_SIZE, unsigned char> CScriptBase;
bool GetScriptOp(CScriptBase::const_iterator& pc, CScriptBase::const_iterator end, opcodetype& opcodeRet, std::vector<unsigned char>* pvchRet);

View file

@ -1160,10 +1160,10 @@ BOOST_AUTO_TEST_CASE(script_CHECKMULTISIG23)
BOOST_AUTO_TEST_CASE(script_size_and_capacity_test)
{
BOOST_CHECK_EQUAL(sizeof(prevector<PREVECTOR_SIZE, unsigned char>), 32);
BOOST_CHECK_EQUAL(sizeof(CScriptBase), 32);
BOOST_CHECK_EQUAL(sizeof(CScript), 32);
BOOST_CHECK_EQUAL(sizeof(CTxOut), 40);
BOOST_CHECK_EQUAL(sizeof(prevector<34, uint8_t>), sizeof(prevector<PREVECTOR_SIZE, uint8_t>));
BOOST_CHECK_EQUAL(sizeof(CScriptBase), 40);
BOOST_CHECK_EQUAL(sizeof(CScript), 40);
BOOST_CHECK_EQUAL(sizeof(CTxOut), 48);
CKey dummyKey;
dummyKey.MakeNewKey(true);
@ -1206,31 +1206,31 @@ BOOST_AUTO_TEST_CASE(script_size_and_capacity_test)
BOOST_CHECK_EQUAL(scriptP2PKH.allocated_memory(), 0);
}
// P2WSH needs extra allocation
// P2WSH has direct allocation
{
const auto scriptP2WSH{GetScriptForDestination(WitnessV0ScriptHash{CScript{} << OP_TRUE})};
BOOST_CHECK(scriptP2WSH.IsPayToWitnessScriptHash());
BOOST_CHECK_EQUAL(scriptP2WSH.size(), 34);
BOOST_CHECK_EQUAL(scriptP2WSH.capacity(), 34);
BOOST_CHECK_EQUAL(scriptP2WSH.allocated_memory(), 34);
BOOST_CHECK_EQUAL(scriptP2WSH.capacity(), PREVECTOR_SIZE);
BOOST_CHECK_EQUAL(scriptP2WSH.allocated_memory(), 0);
}
// P2TR needs extra allocation
// P2TR has direct allocation
{
const auto scriptTaproot{GetScriptForDestination(WitnessV1Taproot{XOnlyPubKey{CPubKey{dummyKey.GetPubKey()}}})};
BOOST_CHECK_EQUAL(Solver(scriptTaproot, dummyVSolutions), TxoutType::WITNESS_V1_TAPROOT);
BOOST_CHECK_EQUAL(scriptTaproot.size(), 34);
BOOST_CHECK_EQUAL(scriptTaproot.capacity(), 34);
BOOST_CHECK_EQUAL(scriptTaproot.allocated_memory(), 34);
BOOST_CHECK_EQUAL(scriptTaproot.capacity(), PREVECTOR_SIZE);
BOOST_CHECK_EQUAL(scriptTaproot.allocated_memory(), 0);
}
// P2PK needs extra allocation
// P2PK has direct allocation
{
const auto scriptPubKey{GetScriptForRawPubKey(CPubKey{dummyKey.GetPubKey()})};
BOOST_CHECK_EQUAL(Solver(scriptPubKey, dummyVSolutions), TxoutType::PUBKEY);
BOOST_CHECK_EQUAL(scriptPubKey.size(), 35);
BOOST_CHECK_EQUAL(scriptPubKey.capacity(), 35);
BOOST_CHECK_EQUAL(scriptPubKey.allocated_memory(), 35);
BOOST_CHECK_EQUAL(scriptPubKey.capacity(), PREVECTOR_SIZE);
BOOST_CHECK_EQUAL(scriptPubKey.allocated_memory(), 0);
}
// MULTISIG needs extra allocation

View file

@ -26,9 +26,8 @@ BOOST_AUTO_TEST_CASE(getcoinscachesizestate)
LOCK(::cs_main);
auto& view = chainstate.CoinsTip();
// The number of bytes consumed by coin's heap data, i.e. CScript
// (prevector<28, unsigned char>) when assigned 56 bytes of data per above.
//
// The number of bytes consumed by coin's heap data, i.e.
// CScript (prevector<36, unsigned char>) when assigned 56 bytes of data per above.
// See also: Coin::DynamicMemoryUsage().
constexpr unsigned int COIN_SIZE = is_64_bit ? 80 : 64;