While running my coin selection simulations, I noticed that towards the end of the simulation, the wallet would become slow to make new transactions. The wallet generally performs much more slowly when there are a large number of transactions and/or a large number of keys. The improvements here are focused on wallets with a large number of transactions as that is what the simulations produce.
Most of the slowdown I observed was due to DescriptorScriptPubKeyMan::GetSigningProvider re-deriving keys every time it is called. To avoid this, it will now cache the SigningProvider produced so that repeatedly fetching the SigningProvider for the same script will not result in the same key being derived over and over. This has a side effect of making the function non-const, which makes a lot of other functions non-const as well. This helps with wallets with lots of address reuse (as my coin selection simulations are), but not if addresses are not reused as keys will end up needing to be derived the first time GetSigningProvider is called for a script.
The GetSigningProvider problem was also exacerbated by unnecessarily fetching a SigningProvider for the same script multiple times. A SigningProvider is retrieved to be used inside of IsSolvable. A few lines later, we use GetTxSpendSize which fetches a SigningProvider and then calls CalculateMaximumSignedInputSize. We can avoid a second call to GetSigningProvider by using CalculateMaximumSignedInputSize directly with the SigningProvider already retrieved for IsSolvable.
There is an additional slowdown where ProduceSignature with a dummy signer is called twice for each output. The first time is IsSolvable checks that ProduceSignature succeeds, thereby informing whether we have solving data. The second is CalculateMaximumSignedInputSize which returns -1 if ProduceSignature fails, and returns the input size otherwise. We can reduce this to one call of ProduceSignature by using CalculateMaximumSignedInputSize’s result to set solvable.
Lastly, a lot of time is spent looking in mapWallet and mapTxSpends to determine whether an output is already spent. The performance of these lookups is slightly improved by changing those maps to use std::unordered_map and std::unordered_multimap respectively.