Skip to content

Commit e66630c

Browse files
committed
Merge bitcoin/bitcoin#13226: Optimize SelectCoinsBnB by tracking the selection by index rather than by position
9d20052 doc: Revise comments and whitespace to clarify (Ben Woosley) def43a4 refactor: Rename i to curr_try in SelectCoinsBnB (Ben Woosley) 1dd0923 refactor: Track BnB selection by index (Ben Woosley) Pull request description: This is prompted by #13167 and presented as a friendly alternative to it. IMO you can improve code readability and performance by about 20% by tracking the selected utxos by index, rather than by position. This reduces the storage access complexity from roughly O(utxo_size) to O(selection_size). On my machine (median of 5 trials): ``` BnBExhaustion, 5, 650, 2.2564, 0.000672999, 0.000711565, 0.000693112 - master BnBExhaustion, 5, 650, 1.76232, 0.000528563, 0.000568806, 0.000539147 - this PR ``` ACKs for top commit: achow101: reACK 9d20052 glozow: code review ACK 9d20052 Xekyo: reACK 9d20052 Tree-SHA512: 453ea11ad58c48928dc76956e3e98916f6924e95510eb02fe89a899ff102fe9cc08a04d557f381ad0218a210275e5383101d971c1ffad38b06b1c57d81144315
2 parents 91d1234 + 9d20052 commit e66630c

File tree

1 file changed

+28
-32
lines changed

1 file changed

+28
-32
lines changed

src/wallet/coinselection.cpp

Lines changed: 28 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,13 @@ std::optional<SelectionResult> SelectCoinsBnB(std::vector<OutputGroup>& utxo_poo
6666
{
6767
SelectionResult result(selection_target);
6868
CAmount curr_value = 0;
69-
70-
std::vector<bool> curr_selection; // select the utxo at this index
71-
curr_selection.reserve(utxo_pool.size());
69+
std::vector<size_t> curr_selection; // selected utxo indexes
7270

7371
// Calculate curr_available_value
7472
CAmount curr_available_value = 0;
7573
for (const OutputGroup& utxo : utxo_pool) {
76-
// Assert that this utxo is not negative. It should never be negative, effective value calculation should have removed it
74+
// Assert that this utxo is not negative. It should never be negative,
75+
// effective value calculation should have removed it
7776
assert(utxo.GetSelectionAmount() > 0);
7877
curr_available_value += utxo.GetSelectionAmount();
7978
}
@@ -85,15 +84,15 @@ std::optional<SelectionResult> SelectCoinsBnB(std::vector<OutputGroup>& utxo_poo
8584
std::sort(utxo_pool.begin(), utxo_pool.end(), descending);
8685

8786
CAmount curr_waste = 0;
88-
std::vector<bool> best_selection;
87+
std::vector<size_t> best_selection;
8988
CAmount best_waste = MAX_MONEY;
9089

9190
// Depth First search loop for choosing the UTXOs
92-
for (size_t i = 0; i < TOTAL_TRIES; ++i) {
91+
for (size_t curr_try = 0, utxo_pool_index = 0; curr_try < TOTAL_TRIES; ++curr_try, ++utxo_pool_index) {
9392
// Conditions for starting a backtrack
9493
bool backtrack = false;
95-
if (curr_value + curr_available_value < selection_target || // Cannot possibly reach target with the amount remaining in the curr_available_value.
96-
curr_value > selection_target + cost_of_change || // Selected value is out of range, go back and try other branch
94+
if (curr_value + curr_available_value < selection_target || // Cannot possibly reach target with the amount remaining in the curr_available_value.
95+
curr_value > selection_target + cost_of_change || // Selected value is out of range, go back and try other branch
9796
(curr_waste > best_waste && (utxo_pool.at(0).fee - utxo_pool.at(0).long_term_fee) > 0)) { // Don't select things which we know will be more wasteful if the waste is increasing
9897
backtrack = true;
9998
} else if (curr_value >= selection_target) { // Selected value is within range
@@ -104,7 +103,6 @@ std::optional<SelectionResult> SelectCoinsBnB(std::vector<OutputGroup>& utxo_poo
104103
// explore any more UTXOs to avoid burning money like that.
105104
if (curr_waste <= best_waste) {
106105
best_selection = curr_selection;
107-
best_selection.resize(utxo_pool.size());
108106
best_waste = curr_waste;
109107
if (best_waste == 0) {
110108
break;
@@ -114,38 +112,38 @@ std::optional<SelectionResult> SelectCoinsBnB(std::vector<OutputGroup>& utxo_poo
114112
backtrack = true;
115113
}
116114

117-
// Backtracking, moving backwards
118-
if (backtrack) {
119-
// Walk backwards to find the last included UTXO that still needs to have its omission branch traversed.
120-
while (!curr_selection.empty() && !curr_selection.back()) {
121-
curr_selection.pop_back();
122-
curr_available_value += utxo_pool.at(curr_selection.size()).GetSelectionAmount();
123-
}
124-
115+
if (backtrack) { // Backtracking, moving backwards
125116
if (curr_selection.empty()) { // We have walked back to the first utxo and no branch is untraversed. All solutions searched
126117
break;
127118
}
128119

120+
// Add omitted UTXOs back to lookahead before traversing the omission branch of last included UTXO.
121+
for (--utxo_pool_index; utxo_pool_index > curr_selection.back(); --utxo_pool_index) {
122+
curr_available_value += utxo_pool.at(utxo_pool_index).GetSelectionAmount();
123+
}
124+
129125
// Output was included on previous iterations, try excluding now.
130-
curr_selection.back() = false;
131-
OutputGroup& utxo = utxo_pool.at(curr_selection.size() - 1);
126+
assert(utxo_pool_index == curr_selection.back());
127+
OutputGroup& utxo = utxo_pool.at(utxo_pool_index);
132128
curr_value -= utxo.GetSelectionAmount();
133129
curr_waste -= utxo.fee - utxo.long_term_fee;
130+
curr_selection.pop_back();
134131
} else { // Moving forwards, continuing down this branch
135-
OutputGroup& utxo = utxo_pool.at(curr_selection.size());
132+
OutputGroup& utxo = utxo_pool.at(utxo_pool_index);
136133

137134
// Remove this utxo from the curr_available_value utxo amount
138135
curr_available_value -= utxo.GetSelectionAmount();
139136

140-
// Avoid searching a branch if the previous UTXO has the same value and same waste and was excluded. Since the ratio of fee to
141-
// long term fee is the same, we only need to check if one of those values match in order to know that the waste is the same.
142-
if (!curr_selection.empty() && !curr_selection.back() &&
143-
utxo.GetSelectionAmount() == utxo_pool.at(curr_selection.size() - 1).GetSelectionAmount() &&
144-
utxo.fee == utxo_pool.at(curr_selection.size() - 1).fee) {
145-
curr_selection.push_back(false);
146-
} else {
137+
if (curr_selection.empty() ||
138+
// The previous index is included and therefore not relevant for exclusion shortcut
139+
(utxo_pool_index - 1) == curr_selection.back() ||
140+
// Avoid searching a branch if the previous UTXO has the same value and same waste and was excluded.
141+
// Since the ratio of fee to long term fee is the same, we only need to check if one of those values match in order to know that the waste is the same.
142+
utxo.GetSelectionAmount() != utxo_pool.at(utxo_pool_index - 1).GetSelectionAmount() ||
143+
utxo.fee != utxo_pool.at(utxo_pool_index - 1).fee)
144+
{
147145
// Inclusion branch first (Largest First Exploration)
148-
curr_selection.push_back(true);
146+
curr_selection.push_back(utxo_pool_index);
149147
curr_value += utxo.GetSelectionAmount();
150148
curr_waste += utxo.fee - utxo.long_term_fee;
151149
}
@@ -158,10 +156,8 @@ std::optional<SelectionResult> SelectCoinsBnB(std::vector<OutputGroup>& utxo_poo
158156
}
159157

160158
// Set output set
161-
for (size_t i = 0; i < best_selection.size(); ++i) {
162-
if (best_selection.at(i)) {
163-
result.AddInput(utxo_pool.at(i));
164-
}
159+
for (const size_t& i : best_selection) {
160+
result.AddInput(utxo_pool.at(i));
165161
}
166162
result.ComputeAndSetWaste(CAmount{0});
167163
assert(best_waste == result.GetWaste());

0 commit comments

Comments
 (0)